about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.env.production.sample7
-rw-r--r--.gitignore3
-rw-r--r--CONTRIBUTING.md13
-rw-r--r--README.md123
-rw-r--r--Vagrantfile6
-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/containers/mastodon.jsx2
-rw-r--r--app/assets/javascripts/components/features/compose/components/character_counter.jsx13
-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/stylesheets/admin.scss2
-rw-r--r--app/assets/stylesheets/components.scss5
-rw-r--r--app/assets/stylesheets/forms.scss18
-rw-r--r--app/controllers/api/v1/statuses_controller.rb8
-rw-r--r--app/controllers/api_controller.rb13
-rw-r--r--app/controllers/auth/registrations_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.rb6
-rw-r--r--app/mailers/application_mailer.rb1
-rw-r--r--app/mailers/user_mailer.rb2
-rw-r--r--app/models/account.rb14
-rw-r--r--app/models/block.rb10
-rw-r--r--app/models/favourite.rb6
-rw-r--r--app/models/media_attachment.rb1
-rw-r--r--app/models/mute.rb14
-rw-r--r--app/models/status.rb5
-rw-r--r--app/models/subscription.rb2
-rw-r--r--app/models/user.rb4
-rw-r--r--app/services/account_search_service.rb2
-rw-r--r--app/services/post_status_service.rb2
-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/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.rabl2
-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
-rw-r--r--config/application.rb4
-rw-r--r--config/environments/production.rb4
-rw-r--r--config/locales/de.yml2
-rw-r--r--config/locales/devise.ja.yml2
-rw-r--r--config/locales/devise.nl.yml72
-rw-r--r--config/locales/devise.pt-BR.yml61
-rw-r--r--config/locales/devise.pt.yml76
-rw-r--r--config/locales/doorkeeper.nl.yml56
-rw-r--r--config/locales/doorkeeper.pt-BR.yml112
-rw-r--r--config/locales/doorkeeper.pt.yml22
-rw-r--r--config/locales/fr.yml33
-rw-r--r--config/locales/ja.yml14
-rw-r--r--config/locales/nl.yml143
-rw-r--r--config/locales/pt-BR.yml198
-rw-r--r--config/locales/pt.yml168
-rw-r--r--config/locales/simple_form.nl.yml36
-rw-r--r--config/locales/simple_form.pt-BR.yml30
-rw-r--r--config/locales/simple_form.pt.yml47
-rw-r--r--config/navigation.rb4
-rw-r--r--config/routes.rb2
-rw-r--r--lib/tasks/mastodon.rake10
-rw-r--r--public/mask-icon.svg1
-rw-r--r--spec/controllers/auth/registrations_controller_spec.rb7
-rw-r--r--spec/controllers/media_controller_spec.rb37
-rw-r--r--spec/helpers/instance_helper_spec.rb33
-rw-r--r--spec/helpers/site_title_helper_spec.rb15
-rw-r--r--spec/i18n_spec.rb16
-rw-r--r--spec/lib/atom_serializer_spec.rb21
-rw-r--r--spec/models/account_spec.rb17
-rw-r--r--spec/requests/webfinger_request_spec.rb45
-rw-r--r--spec/routing/well_known_routes_spec.rb2
-rw-r--r--spec/services/account_search_service_spec.rb12
-rw-r--r--storybook/config.js1
-rw-r--r--storybook/stories/character_counter.story.jsx20
-rw-r--r--streaming/index.js468
97 files changed, 1969 insertions, 892 deletions
diff --git a/.env.production.sample b/.env.production.sample
index 9011dc217..28afee246 100644
--- a/.env.production.sample
+++ b/.env.production.sample
@@ -35,11 +35,14 @@ OTP_SECRET=
 
 # E-mail configuration
 # Note: Mailgun and SparkPost (https://sparkpo.st/smtp) each have good free tiers
+# If you want to use an SMTP server without authentication (e.g local Postfix relay)
+# then set SMTP_AUTH_METHOD to 'none' and leave SMTP_LOGIN and SMTP_PASSWORD blank
 SMTP_SERVER=smtp.mailgun.org
 SMTP_PORT=587
 SMTP_LOGIN=
 SMTP_PASSWORD=
 SMTP_FROM_ADDRESS=notifications@example.com
+#SMTP_DOMAIN= # defaults to LOCAL_DOMAIN
 #SMTP_DELIVERY_METHOD=smtp # delivery method can also be sendmail
 #SMTP_AUTH_METHOD=plain
 #SMTP_OPENSSL_VERIFY_MODE=peer
@@ -81,3 +84,7 @@ SMTP_FROM_ADDRESS=notifications@example.com
 # Advanced settings
 # If you need to use pgBouncer, you need to disable prepared statements:
 # PREPARED_STATEMENTS=false
+
+# Cluster number setting for streaming API server.
+# If you comment out following line, cluster number will be `numOfCpuCores - 1`.
+STREAMING_CLUSTER_NUM=1
diff --git a/.gitignore b/.gitignore
index cda6b87b3..c6c468cc7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,8 +4,9 @@
 # or operating system, you probably want to add a global ignore instead:
 #   git config --global core.excludesfile '~/.gitignore_global'
 
-# Ignore bundler config.
+# Ignore bundler config and downloaded libraries.
 /.bundle
+/vendor/bundle
 
 # Ignore the default SQLite database.
 /db/*.sqlite3
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9ca01a56f..299306299 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -7,7 +7,7 @@ There are three ways in which you can contribute to this repository:
 2. By working on the back-end application
 3. By working on the front-end application
 
-Choosing what to work on in a large open source project is not easy. The list of GitHub issues may provide some ideas, but not every feature request has been greenlit. Likewise, not every change or feature that resolves a personal itch will be merged into the main repository. Some communication ahead of time may be wise. If your addition creates a new feature or setting, or otherwise changes how things work in some substantial way, please remember to submit a correlating pull request to document your changes in the [documentation](http://github.com/tootsuite/documentation).
+Choosing what to work on in a large open source project is not easy. The list of [GitHub issues](https://github.com/tootsuite/mastodon/issues) may provide some ideas, but not every feature request has been greenlit. Likewise, not every change or feature that resolves a personal itch will be merged into the main repository. Some communication ahead of time may be wise. If your addition creates a new feature or setting, or otherwise changes how things work in some substantial way, please remember to submit a correlating pull request to document your changes in the [documentation](http://github.com/tootsuite/documentation).
 
 Below are the guidelines for working on pull requests:
 
@@ -21,9 +21,17 @@ Below are the guidelines for working on pull requests:
 - No orthographic mistakes
 - No Markdown syntax errors
 
+## Requirements
+
+- Ruby
+- Node.js
+- PostgreSQL
+- Redis
+- Nginx (optional)
+
 ## Back-end application
 
-It is expected that you have a working development environment set up. The development environment includes rubocop, which checks your Ruby code for compliance with our style guide and best practices. Sublime Text, likely like other editors, has a Rubocop plugin that runs checks on files as you edit them. The codebase also has a test suite.
+It is expected that you have a working development environment set up. The development environment includes [rubocop](https://github.com/bbatsov/rubocop), which checks your Ruby code for compliance with our style guide and best practices. Sublime Text, likely like other editors, has a [Rubocop plugin](https://github.com/pderichs/sublime_rubocop) that runs checks on files as you edit them. The codebase also has a test suite.
 
 * The codebase is not perfect, at the time of writing, but it is expected that you do not introduce new code style violations
 * The rspec test suite must pass
@@ -41,4 +49,3 @@ It is expected that you have a working development environment set up (see back-
 * If you are introducing new strings, they must be using localization methods
 
 If the JavaScript or CSS assets won't compile due to a syntax error, it's a good sign that the pull request isn't ready for submission yet.
-
diff --git a/README.md b/README.md
index ea7b20a5c..804ad394b 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ Mastodon
 
 Mastodon is a free, open-source social network server. A decentralized solution to commercial platforms, it avoids the risks of a single company monopolizing your communication. Anyone can run Mastodon and participate in the social network seamlessly.
 
-An alternative implementation of the GNU social project. Based on ActivityStreams, Webfinger, PubsubHubbub and Salmon.
+An alternative implementation of the GNU social project. Based on [ActivityStreams](https://en.wikipedia.org/wiki/Activity_Streams_(format)), [Webfinger](https://en.wikipedia.org/wiki/WebFinger), [PubsubHubbub](https://en.wikipedia.org/wiki/PubSubHubbub) and [Salmon](https://en.wikipedia.org/wiki/Salmon_(protocol)).
 
 Click on the screenshot to watch a demo of the UI:
 
@@ -48,126 +48,9 @@ If you would like, you can [support the development of this project on Patreon][
 - **Deployable via Docker**
   You don't need to mess with dependencies and configuration if you want to try Mastodon, if you have Docker and Docker Compose the deployment is extremely easy
 
-## Checking out
+## Deployment
 
-If you want a stable release for production use, you should use tagged releases. To checkout the latest available tagged version:
-
-    git clone https://github.com/tootsuite/mastodon.git
-    cd mastodon
-    git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
-
-## Configuration
-
-- `LOCAL_DOMAIN` should be the domain/hostname of your instance. This is **absolutely required** as it is used for generating unique IDs for everything federation-related
-- `LOCAL_HTTPS` set it to `true` if HTTPS works on your website. This is used to generate canonical URLs, which is also important when generating and parsing federation-related IDs
-
-Consult the example configuration file, `.env.production.sample` for the full list. Among other things you need to set details for the SMTP server you are going to use.
-
-## Requirements
-
-- Ruby
-- Node.js
-- PostgreSQL
-- Redis
-- Nginx
-
-## Running with Docker and Docker-Compose
-
-[![](https://images.microbadger.com/badges/version/gargron/mastodon.svg)](https://microbadger.com/images/gargron/mastodon "Get your own version badge on microbadger.com") [![](https://images.microbadger.com/badges/image/gargron/mastodon.svg)](https://microbadger.com/images/gargron/mastodon "Get your own image badge on microbadger.com")
-
-The project now includes a `Dockerfile` and a `docker-compose.yml` file (which requires at least docker-compose version `1.10.0`).
-
-Review the settings in `docker-compose.yml`. Note that it is not default to store the postgresql database and redis databases in a persistent storage location,
-so you may need or want to adjust the settings there.
-
-Then, you need to fill in the `.env.production` file:
-
-    cp .env.production.sample .env.production
-    nano .env.production
-
-Do NOT change the `REDIS_*` or `DB_*` settings when running with the default docker configurations.
-
-You will need to fill in, at least: `LOCAL_DOMAIN`, `LOCAL_HTTPS`, `PAPERCLIP_SECRET`, `SECRET_KEY_BASE`, `OTP_SECRET`, and the `SMTP_*` settings.  To generate the `PAPERCLIP_SECRET`, `SECRET_KEY_BASE`, and `OTP_SECRET`, you may use:
-
-Before running the first time, you need to build the images:
-
-    docker-compose build
-
-
-    docker-compose run --rm web rake secret
-
-Do this once for each of those keys, and copy the result into the `.env.production` file in the appropriate field.
-
-Then you should run the `db:migrate` command to create the database, or migrate it from an older release:
-
-    docker-compose run --rm web rails db:migrate
-
-Then, you will also need to precompile the assets:
-
-    docker-compose run --rm web rails assets:precompile
-
-before you can launch the docker image with:
-
-    docker-compose up
-
-If you wish to run this as a daemon process instead of monitoring it on console, use instead:
-
-    docker-compose up -d
-
-Then you may login to your new Mastodon instance by browsing to http://localhost:3000/
-
-Following that, make sure that you read the [production guide](docs/Running-Mastodon/Production-guide.md). You are probably going to want to understand how
-to configure Nginx to make your Mastodon instance available to the rest of the world.
-
-The container has two volumes, for the assets and for user uploads, and optionally two more, for the postgresql and redis databases.
-
-The default docker-compose.yml maps them to the repository's `public/assets` and `public/system` directories, you may wish to put them somewhere else. Likewise, the PostgreSQL and Redis images have data containers that you may wish to map somewhere where you know how to find them and back them up.
-
-**Note**: The `--rm` option for docker-compose will remove the container that is created to run a one-off command after it completes. As data is stored in volumes it is not affected by that container clean-up.
-
-### Tasks
-
-- `rake mastodon:media:clear` removes uploads that have not been attached to any status after a while, you would want to run this from a periodic cronjob
-- `rake mastodon:push:clear` unsubscribes from PuSH notifications for remote users that have no local followers. You may not want to actually do that, to keep a fuller footprint of the fediverse or in case your users will soon re-follow
-- `rake mastodon:push:refresh` re-subscribes PuSH for expiring remote users, this should be run periodically from a cronjob and quite often as the expiration time depends on the particular hub of the remote user
-- `rake mastodon:feeds:clear_all` removes all timelines, which forces them to be re-built on the fly next time a user tries to fetch their home/mentions timeline. Only for troubleshooting
-- `rake mastodon:feeds:clear` removes timelines of users who haven't signed in lately, which allows to save RAM and improve message distribution. This is required to be run periodically so that when they login again the regeneration process will trigger
-
-Running any of these tasks via docker-compose would look like this:
-
-    docker-compose run --rm web rake mastodon:media:clear
-
-### Updating
-
-This approach makes updating to the latest version a real breeze.
-
-1. `git pull` to download updates from the repository
-2. `docker-compose build` to compile the Docker image out of the changed source files
-3. (optional) `docker-compose run --rm web rails db:migrate` to perform database migrations. Does nothing if your database is up to date
-4. (optional) `docker-compose run --rm web rails assets:precompile` to compile new JS and CSS assets
-5. `docker-compose up -d` to re-create (restart) containers and pick up the changes
-
-## Deployment without Docker
-
-Docker is great for quickly trying out software, but it has its drawbacks too. If you prefer to run Mastodon without using Docker, refer to the [production guide](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Production-guide.md) for examples, configuration and instructions.
-
-## Deployment on Scalingo
-
-[![Deploy on Scalingo](https://cdn.scalingo.com/deploy/button.svg)](https://my.scalingo.com/deploy?source=https://github.com/tootsuite/mastodon#master)
-
-[You can view a guide for deployment on Scalingo here.](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Scalingo-guide.md)
-
-## Deployment on Heroku (experimental)
-
-[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy)
-
-Mastodon can run on [Heroku](https://heroku.com), but it gets expensive and impractical due to how Heroku prices resource usage. [You can view a guide for deployment on Heroku here](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Heroku-guide.md), but you have been warned.
-
-## Development with Vagrant
-
-A quick way to get a development environment up and running is with Vagrant. You will need recent versions of [Vagrant](https://www.vagrantup.com/) and [VirtualBox](https://www.virtualbox.org/) installed.
-
-[You can find the guide for setting up a Vagrant development environment here.](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Vagrant-guide.md)
+There are guides in the documentation repository for [deploying on various platforms](https://github.com/tootsuite/documentation#running-mastodon).
 
 ## Contributing
 
diff --git a/Vagrantfile b/Vagrantfile
index 66892e443..9047037bc 100644
--- a/Vagrantfile
+++ b/Vagrantfile
@@ -107,7 +107,11 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
     config.hostsupdater.remove_on_suspend = false
   end
 
-  config.vm.synced_folder ".", "/vagrant", type: "nfs", mount_options: ['rw', 'vers=3', 'tcp']
+  if config.vm.networks.any? { |type, options| type == :private_network }
+    config.vm.synced_folder ".", "/vagrant", type: "nfs", mount_options: ['rw', 'vers=3', 'tcp']
+  else
+    config.vm.synced_folder ".", "/vagrant"
+  end
 
   # Otherwise, you can access the site at http://localhost:3000
   config.vm.network :forwarded_port, guest: 80, host: 3000
diff --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/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx
index 08576913e..185911861 100644
--- a/app/assets/javascripts/components/containers/mastodon.jsx
+++ b/app/assets/javascripts/components/containers/mastodon.jsx
@@ -57,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';
@@ -79,6 +80,7 @@ addLocaleData([
   ...hu,
   ...ja,
   ...pt,
+  ...pt_br,
   ...nl,
   ...no,
   ...ru,
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/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/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 84344e94d..6f407a6d5 100644
--- a/app/assets/stylesheets/components.scss
+++ b/app/assets/stylesheets/components.scss
@@ -2050,8 +2050,8 @@ button.icon-button.active i.fa-retweet {
 .onboarding-modal__pager {
   height: 80vh;
   width: 80vw;
-  max-width: 500px;
-  max-height: 350px;
+  max-width: 520px;
+  max-height: 420px;
   position: relative;
 
   & > div {
@@ -2281,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/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/api_controller.rb b/app/controllers/api_controller.rb
index 57604f1dc..478d21bc7 100644
--- a/app/controllers/api_controller.rb
+++ b/app/controllers/api_controller.rb
@@ -100,17 +100,4 @@ class ApiController < ApplicationController
     @reblogs_map    = Status.reblogs_map(status_ids, current_account)
     @favourites_map = Status.favourites_map(status_ids, current_account)
   end
-
-  def set_counters_maps(statuses) # rubocop:disable Style/AccessorMethodName
-    status_ids             = statuses.compact.map { |s| s.reblog? ? s.reblog_of_id : s.id }.uniq
-    @favourites_counts_map = Favourite.select('status_id, COUNT(id) AS favourites_count').group('status_id').where(status_id: status_ids).map { |f| [f.status_id, f.favourites_count] }.to_h
-    @reblogs_counts_map    = Status.select('statuses.id, COUNT(reblogs.id) AS reblogs_count').joins('LEFT OUTER JOIN statuses AS reblogs ON statuses.id = reblogs.reblog_of_id').where(id: status_ids).group('statuses.id').map { |r| [r.id, r.reblogs_count] }.to_h
-  end
-
-  def set_account_counters_maps(accounts) # rubocop:disable Style/AccessorMethodName
-    account_ids = accounts.compact.map(&:id).uniq
-    @followers_counts_map = Follow.unscoped.select('target_account_id, COUNT(account_id) AS followers_count').group('target_account_id').where(target_account_id: account_ids).map { |f| [f.target_account_id, f.followers_count] }.to_h
-    @following_counts_map = Follow.unscoped.select('account_id, COUNT(target_account_id) AS following_count').group('account_id').where(account_id: account_ids).map { |f| [f.account_id, f.following_count] }.to_h
-    @statuses_counts_map  = Status.unscoped.select('account_id, COUNT(id) AS statuses_count').group('account_id').where(account_id: account_ids).map { |s| [s.account_id, s.statuses_count] }.to_h
-  end
 end
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/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 4e4031bba..5aeb7b4f9 100644
--- a/app/lib/atom_serializer.rb
+++ b/app/lib/atom_serializer.rb
@@ -3,13 +3,11 @@
 class AtomSerializer
   include RoutingHelper
 
-  INVALID_XML_CHARS = /[^\u0009\u000a\u000d\u0020-\uD7FF\uE000-\uFFFD\u10000-\u10FFFF]/
-
   class << self
     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
 
@@ -319,7 +317,7 @@ class AtomSerializer
   end
 
   def sanitize_str(raw_str)
-    raw_str.to_s.gsub(INVALID_XML_CHARS, '')
+    raw_str.to_s
   end
 
   def add_namespaces(parent)
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/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/account.rb b/app/models/account.rb
index 8ceda7f97..259a87451 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -108,10 +108,6 @@ class Account < ApplicationRecord
     follow_requests.where(target_account: other_account).exists?
   end
 
-  def followers_domains
-    followers.reorder('').select('DISTINCT accounts.domain').map(&:domain)
-  end
-
   def local?
     domain.nil?
   end
@@ -231,18 +227,20 @@ class Account < ApplicationRecord
         WITH first_degree AS (
             SELECT target_account_id
             FROM follows
-            WHERE account_id = ?
+            WHERE account_id = :account_id
           )
         SELECT accounts.*
         FROM follows
         INNER JOIN accounts ON follows.target_account_id = accounts.id
-        WHERE account_id IN (SELECT * FROM first_degree) AND target_account_id NOT IN (SELECT * FROM first_degree) AND target_account_id <> ?
+        WHERE account_id IN (SELECT * FROM first_degree) AND target_account_id NOT IN (SELECT * FROM first_degree) AND target_account_id <> :account_id
         GROUP BY target_account_id, accounts.id
         ORDER BY count(account_id) DESC
-        LIMIT ?
+        LIMIT :limit
       SQL
 
-      Account.find_by_sql([sql, account.id, account.id, limit])
+      find_by_sql(
+        [sql, { account_id: account.id, limit: limit }]
+      )
     end
 
     def search_for(terms, limit = 10)
diff --git a/app/models/block.rb b/app/models/block.rb
index ae456a6b6..c978b2200 100644
--- a/app/models/block.rb
+++ b/app/models/block.rb
@@ -7,4 +7,14 @@ class Block < ApplicationRecord
   belongs_to :target_account, class_name: 'Account', required: true
 
   validates :account_id, uniqueness: { scope: :target_account_id }
+
+  after_create  :remove_blocking_cache
+  after_destroy :remove_blocking_cache
+
+  private
+
+  def remove_blocking_cache
+    Rails.cache.delete("exclude_account_ids_for:#{account_id}")
+    Rails.cache.delete("exclude_account_ids_for:#{target_account_id}")
+  end
 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 a5b334c85..d0de62ed5 100644
--- a/app/models/mute.rb
+++ b/app/models/mute.rb
@@ -3,9 +3,17 @@
 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
+  after_destroy :remove_blocking_cache
+
+  private
+
+  def remove_blocking_cache
+    Rails.cache.delete("exclude_account_ids_for:#{account_id}")
+  end
 end
diff --git a/app/models/status.rb b/app/models/status.rb
index c0a5d9d1b..a9b7327c3 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
@@ -184,7 +183,7 @@ class Status < ApplicationRecord
     private
 
     def filter_timeline(query, account)
-      blocked = Block.where(account: account).pluck(:target_account_id) + Block.where(target_account: account).pluck(:account_id) + Mute.where(account: account).pluck(:target_account_id)
+      blocked = Rails.cache.fetch("exclude_account_ids_for:#{account.id}") { Block.where(account: account).pluck(:target_account_id) + Block.where(target_account: account).pluck(:account_id) + Mute.where(account: account).pluck(:target_account_id) }
       query   = query.where('statuses.account_id NOT IN (?)', blocked) unless blocked.empty?  # Only give us statuses from people we haven't blocked, or muted, or that have blocked us
       query   = query.where('accounts.silenced = TRUE') if account.silenced?                  # and if we're hellbanned, only people who are also hellbanned
       query
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 d50101baf..cd1f816ca 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -9,14 +9,12 @@ 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
 
-  scope :prolific,  -> { joins('inner join statuses on statuses.account_id = users.account_id').select('users.*, count(statuses.id) as statuses_count').group('users.id').order('statuses_count desc') }
   scope :recent,    -> { order('id desc') }
   scope :admins,    -> { where(admin: true) }
   scope :confirmed, -> { where.not(confirmed_at: nil) }
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/post_status_service.rb b/app/services/post_status_service.rb
index ec2b90e07..00af28edd 100644
--- a/app/services/post_status_service.rb
+++ b/app/services/post_status_service.rb
@@ -36,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
 
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/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 a2ab2d060..b599b5cf0 100644
--- a/app/views/home/initial_state.json.rabl
+++ b/app/views/home/initial_state.json.rabl
@@ -5,7 +5,7 @@ node(:meta) do
     streaming_api_base_url: @streaming_api_base_url,
     access_token: @token,
     locale: I18n.locale,
-    domain: Rails.configuration.x.local_domain,
+    domain: site_hostname,
     me: current_account.id,
     admin: @admin.try(:id),
     boost_modal: current_account.user.setting_boost_modal,
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 3fdcca041..ce3929629 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) %>
 
-このメールに見に覚えのない場合は無視してください。
+このメールに身に覚えのない場合は無視してください。
 上記のリンクにアクセスし、変更をしない限りパスワードは変更されません。
diff --git a/config/application.rb b/config/application.rb
index fa7098b39..396ac33f1 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -32,12 +32,14 @@ module Mastodon
       :es,
       :fi,
       :fr,
-      :it,
+      :hr,
       :hu,
+      :it,
       :ja,
       :nl,
       :no,
       :pt,
+      :'pt-BR',
       :ru,
       :uk,
       'zh-CN',
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 80021287a..a4cdb2732 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -100,8 +100,10 @@ Rails.application.configure do
     :address              => ENV['SMTP_SERVER'],
     :user_name            => ENV['SMTP_LOGIN'],
     :password             => ENV['SMTP_PASSWORD'],
+
     :domain               => ENV['SMTP_DOMAIN'] || ENV['LOCAL_DOMAIN'],
-    :authentication       => ENV['SMTP_AUTH_METHOD'] || :plain,
+    :authentication       => ENV['SMTP_AUTH_METHOD'] == 'none' ? nil : ENV['SMTP_AUTH_METHOD'] || :plain,
+
     :openssl_verify_mode  => ENV['SMTP_OPENSSL_VERIFY_MODE'],
     :enable_starttls_auto => ENV['SMTP_ENABLE_STARTTLS_AUTO'] || true,
   }
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 75ac4e1bb..dcbeea745 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -1,7 +1,7 @@
 ---
 de:
   about:
-    about_mastodon: Mastodon ist ein <em>freier, quelloffener</em> soziales Netzwerkserver. Als <em>dezentralisierte</em> Alternative zu kommerziellen Plattformen verhindert es die Risiken, die entstehen, wenn eine einzelne Firma deine Kommunikation monopolisiert. Jeder kann Mastodon verwenden und ganz einfach am <em>sozialen Netzwerk</em> teilnehmen.
+    about_mastodon: Mastodon ist ein <em>freier, quelloffener</em> sozialer Netzwerkserver. Als <em>dezentralisierte</em> Alternative zu kommerziellen Plattformen verhindert es die Risiken, die entstehen, wenn eine einzelne Firma deine Kommunikation monopolisiert. Jeder kann Mastodon verwenden und ganz einfach am <em>sozialen Netzwerk</em> teilnehmen.
     get_started: Erste Schritte
     source_code: Quellcode
     terms: AGB
diff --git a/config/locales/devise.ja.yml b/config/locales/devise.ja.yml
index 4aeb09cd4..c09c15bb4 100644
--- a/config/locales/devise.ja.yml
+++ b/config/locales/devise.ja.yml
@@ -7,7 +7,7 @@ ja:
       send_paranoid_instructions: もしあなたのメールアドレスが登録されていれば、まもなくメールアドレスの確認の方法が記載されたメールが送信されます。
     failure:
       already_authenticated: 既にログイン済みです。
-      inactive: あなたのアカウントはまだアクティベートされていません。
+      inactive: あなたのアカウントはまだ有効化されていません。
       invalid: '%{authentication_keys}かパスワードが誤っています'
       last_attempt: あと1回失敗するとアカウントがロックされます。
       locked: アカウントはロックされました。
diff --git a/config/locales/devise.nl.yml b/config/locales/devise.nl.yml
index 9057a6775..28e012dfb 100644
--- a/config/locales/devise.nl.yml
+++ b/config/locales/devise.nl.yml
@@ -3,57 +3,59 @@ nl:
   devise:
     confirmations:
       confirmed: Je account is bevestigd.
-      send_instructions: Je ontvangt via e-mail instructies hoe je je account kan bevestigen.
-      send_paranoid_instructions: Als je e-mailadres bestaat in de database, ontvang je via e-mail instructies hoe je je account kan bevestigen.
+      send_instructions: Je ontvangt via e-mail instructies hoe je jouw account kan bevestigen.
+      send_paranoid_instructions: Als jouw e-mailadres in de database staat, ontvang je via e-mail instructies hoe je jouw account kan bevestigen.
     failure:
       already_authenticated: Je bent al ingelogd.
-      inactive: Je account is nog niet geactiveerd.
-      invalid: Ongeldig e-mail of wachtwoord.
-      invalid_token: Invalide authenticiteit token.
-      last_attempt: Je hebt nog een poging over voordat je account wordt geblokkeerd.
-      locked: Je account is gelocked.
-      not_found_in_database: Ongeldig e-mail of wachtwoord.
-      timeout: Je sessie is verlopen, log a.u.b. opnieuw in.
-      unauthenticated: Je dient in te loggen of je in te schrijven.
-      unconfirmed: Je dient eerst je account te bevestigen.
+      inactive: Jouw account is nog niet geactiveerd.
+      invalid: Ongeldig e-mailadres of wachtwoord.
+      invalid_token: Ongeldige bevestigingscode.
+      last_attempt: Je hebt nog één poging over voordat jouw account geblokkeerd wordt.
+      locked: Jouw account is geblokkeerd.
+      not_found_in_database: Ongeldig e-mailadres of wachtwoord.
+      timeout: Jouw sessie is verlopen, log opnieuw in.
+      unauthenticated: Je dient in te loggen of te registreren.
+      unconfirmed: Je dient eerst jouw account te bevestigen.
     mailer:
       confirmation_instructions:
-        subject: Bevestiging mailadres
+        subject: 'Mastodon: E-mail bevestigen voor %{instance}'
+      password_change:
+        subject: 'Mastodon: Wachtwoord veranderd'
       reset_password_instructions:
-        subject: Wachtwoord resetten
+        subject: 'Mastodon: Wachtwoord opnieuw instellen'
       unlock_instructions:
-        subject: Unlock instructies
+        subject: 'Mastodon: Instructies om account te deblokkeren' 
     omniauth_callbacks:
-      failure: Kon je niet aanmelden met je %{kind} account, omdat "%{reason}".
-      success: Successvol aangemeld met je %{kind} account.
+      failure: Kon je niet aanmelden met jouw %{kind} account, omdat "%{reason}".
+      success: Successvol aangemeld met jouw %{kind} account.
     passwords:
-      no_token: Je kan deze pagina niet benaderen zonder een "wachtwoord reset e-mail"
-      send_instructions: Je ontvangt via e-mail instructies hoe je je wachtwoord moet resetten.
-      send_paranoid_instructions: Als je e-mailadres bestaat in de database, ontvang je via e-mail instructies hoe je je wachtwoord moet resetten.
-      updated: Je wachtwoord is gewijzigd. Je bent nu ingelogd.
-      updated_not_active: Je wachtwoord is gewijzigd.
+      no_token: Je kan deze pagina niet benaderen zonder dat je een e-mail om je wachtwoord opnieuw in te stellen hebt ontvangen.
+      send_instructions: Je ontvangt via e-mail instructies hoe je jouw wachtwoord opnieuw moet instellen.
+      send_paranoid_instructions: Als jouw e-mailadres in de database staat, ontvang je via e-mail instructies hoe je jouw wachtwoord opnieuw moet instellen.
+      updated: Jouw wachtwoord is gewijzigd. Je bent nu ingelogd.
+      updated_not_active: Jouw wachtwoord is gewijzigd.
     registrations:
-      destroyed: Je account is verwijderd, wellicht tot ziens!
-      signed_up: Je bent ingeschreven.
-      signed_up_but_inactive: Je bent ingeschreven. Je kon alleen niet automatisch ingelogd worden omdat je account nog niet geactiveerd is.
-      signed_up_but_locked: Je bent ingeschreven. Je kon alleen niet automatisch ingelogd worden omdat je account geblokkeerd is.
-      signed_up_but_unconfirmed: Je ontvangt via e-mail instructies hoe je je account kunt activeren.
-      update_needs_confirmation: Je hebt je e-mailadres succesvol gewijzigd, maar we moeten je nieuwe mailadres nog verifiëren. Controleer je e-mail en klik op de link in de mail om je mailadres te verifiëren.
-      updated: Je account gegevens zijn opgeslagen.
+      destroyed: Jouw account is verwijderd. Wellicht tot ziens!
+      signed_up: Je bent geregistreerd.
+      signed_up_but_inactive: Je bent geregistreerd. Je kon alleen niet automatisch ingelogd worden omdat jouw account nog niet geactiveerd is.
+      signed_up_but_locked: Je bent ingeschreven. Je kon alleen niet automatisch ingelogd worden omdat jouw account geblokkeerd is.
+      signed_up_but_unconfirmed: Je ontvangt via e-mail instructies hoe je jouw account kunt activeren.
+      update_needs_confirmation: Je hebt je e-mailadres succesvol gewijzigd, maar we moeten je nieuwe mailadres nog bevestigen. Controleer jouw e-mail en klik op de link in de mail om jouw e-mailadres te bevestigen.
+      updated: Jouw accountgegevens zijn opgeslagen.
     sessions:
       signed_in: Je bent succesvol ingelogd.
       signed_out: Je bent succesvol uitgelogd.
     unlocks:
-      send_instructions: Je ontvangt via e-mail instructies hoe je je account kan unlocken.
-      send_paranoid_instructions: Als je e-mailadres bestaat in de database, ontvang je via e-mail instructies hoe je je account kan unlocken.
-      unlocked: Je account is ge-unlocked. Je kan nu weer inloggen.
+      send_instructions: Je ontvangt via e-mail instructies hoe je jouw account kan deblokkeren.
+      send_paranoid_instructions: Als jouw e-mailadres in de database staat, ontvang je via e-mail instructies hoe je jouw account kan deblokkeren.
+      unlocked: Jouw account is gedeblokkeerd. Je kan nu weer inloggen.
   errors:
     messages:
       already_confirmed: is reeds bevestigd
-      confirmation_period_expired: moet worden bevestigd binnen %{period}, probeer het a.u.b. nog een keer
+      confirmation_period_expired: moet worden bevestigd binnen %{period}, probeer het nog een keer
       expired: is verlopen, vraag een nieuwe aan
       not_found: niet gevonden
-      not_locked: is niet gesloten
+      not_locked: is niet geblokkeerd
       not_saved:
-        one: '1 fout blokkeerde het opslaan van deze %{resource}:'
-        other: "%{count} fouten blokkeerden het opslaan van deze %{resource}:"
+        one: '1 fout verhinderde het opslaan van deze %{resource}:'
+        other: "%{count} fouten verhinderden het opslaan van deze %{resource}:"
diff --git a/config/locales/devise.pt-BR.yml b/config/locales/devise.pt-BR.yml
new file mode 100644
index 000000000..c647fabbd
--- /dev/null
+++ b/config/locales/devise.pt-BR.yml
@@ -0,0 +1,61 @@
+---
+pt-BR:
+  devise:
+    confirmations:
+      confirmed: O seu endereço de email foi confirmado.
+      send_instructions: Você irá receber um email com instruções em como confirmar o seu endereço de email dentro de alguns minutos.
+      send_paranoid_instructions: Se o seu endereço de email já existir na nossa base de dados, irá receber um email com instruções em como confirmá-lo dentro de alguns minutos.
+    failure:
+      already_authenticated: A sua sessão já está aberta.
+      inactive: A sua contra ainda não está ativada.
+      invalid: "%{authentication_keys} ou password inválidos."
+      last_attempt: Tem mais uma tentativa antes de a sua conta ser protegida.
+      locked: A sua conta está protegida
+      not_found_in_database: "%{authentication_keys} ou password inválidos."
+      timeout: A sua sessão expirou. Por favore entre de novo para continuar.
+      unauthenticated: Você precsa de entrar ou registar-se antes de continuar.
+      unconfirmed: Você tem de confirmar o seu endereço de email antes de continuar.
+    mailer:
+      confirmation_instructions:
+        subject: 'Mastodon: Instruções de confirmação'
+      password_change:
+        subject: 'Mastodon: Password nova'
+      reset_password_instructions:
+        subject: 'Mastodon: Instruções para editar a password'
+      unlock_instructions:
+        subject: 'Mastodon: Instruções para desproteger a sua conta'
+    omniauth_callbacks:
+      failure: Could not authenticate you from %{kind} because "%{reason}".
+      success: Successfully authenticated from %{kind} account.
+    passwords:
+      no_token: Você não pode aceder a esta página sem ter vindo de um email para mudar a password. Se este for o case, por favor faça questão de verificar que usou o URL no email.
+      send_instructions: Irá receber um email com instruções em como mudar a sua password dentro de algns minutos.
+      send_paranoid_instructions: Se  seu endereço de email existe na nossa base de dados, irá receber um link para recuperar a sua password dentro de alguns minutos.
+      updated: A sua password foi alterada. A sua sessão está aberta.
+      updated_not_active: A sua password foi alterada.
+    registrations:
+      destroyed: Adeus! A sua conta foi cancelada. Esperamos vê-lo em breve.
+      signed_up: Bem vindo! A sua conta foi registada com sucesso.
+      signed_up_but_inactive: A sua conta foi registada. No entanto, não abrimos a sua sessão porque a sua conta ainda não foi ativada.
+      signed_up_but_locked: A sua conta foi registada. No entanto, não abrimos a sua sessão porque a sua conta está protegida.
+      signed_up_but_unconfirmed: Uma mensagem com um link de confirmação foi enviada para o seu email. Por favor siga o link para ativar a sua conta.
+      update_needs_confirmation: Você mudou o seu endereço de email ou password, mas é necessário confirmar a mudança. Por favor siga o link que foi enviado para o seu novo endereço de email.
+      updated: A sua conta foi alterada com sucesso.
+    sessions:
+      already_signed_out: Sessão fechada.
+      signed_in: Sessão iniciada.
+      signed_out: Sessão fechada.
+    unlocks:
+      send_instructions: Irá receber um email com instruções para desproteger a sua conta dentro de alguns minutos.
+      send_paranoid_instructions: Se a sua conta existe, irá receber um email com instruções a detalhar como a desproteger dentro de alguns minutos.
+      unlocked: A sua conta foi desprotegida. Por favor inicie sessão para continuar.
+  errors:
+    messages:
+      already_confirmed: já foi confirmado, por favor tente iniciar sessão
+      confirmation_period_expired: tem de ser confirmado dentro de %{period}, por favor tente outra vez
+      expired: expirou, por favor tente outra vez
+      not_found: não encontrado
+      not_locked: não está protegido
+      not_saved:
+        one: '1 erro impediu este %{resource} de ser guardado:'
+        other: "%{count} erros impediram este %{resource} de ser guardado:"
diff --git a/config/locales/devise.pt.yml b/config/locales/devise.pt.yml
index 8c049ce8b..dc87cefdd 100644
--- a/config/locales/devise.pt.yml
+++ b/config/locales/devise.pt.yml
@@ -2,60 +2,60 @@
 pt:
   devise:
     confirmations:
-      confirmed: O seu endereço de email foi confirmado.
-      send_instructions: Você irá receber um email com instruções em como confirmar o seu endereço de email dentro de alguns minutos.
-      send_paranoid_instructions: Se o seu endereço de email já existir na nossa base de dados, irá receber um email com instruções em como confirmá-lo dentro de alguns minutos.
+      confirmed: O teu endereço de email foi confirmado.
+      send_instructions: Vais receber um email com as instruções para confirmar o teu endereço de email dentro de alguns minutos.
+      send_paranoid_instructions: Se o teu endereço de email já existir na nossa base de dados, vais receber um email com as instruções de confirmação dentro de alguns minutos.
     failure:
-      already_authenticated: A sua sessão já está aberta.
-      inactive: A sua contra ainda não está ativada.
-      invalid: "%{authentication_keys} ou password inválidos."
-      last_attempt: Tem mais uma tentativa antes de a sua conta ser protegida.
-      locked: A sua conta está protegida
-      not_found_in_database: "%{authentication_keys} ou password inválidos."
-      timeout: A sua sessão expirou. Por favore entre de novo para continuar.
-      unauthenticated: Você precsa de entrar ou registar-se antes de continuar.
-      unconfirmed: Você tem de confirmar o seu endereço de email antes de continuar.
+      already_authenticated: A tua sessão já está aberta.
+      inactive: A tua conta ainda não está ativada.
+      invalid: "%{authentication_keys} ou palavra-passe não válida."
+      last_attempt: Tens mais uma tentativa antes de a tua conta ficar bloqueada.
+      locked: A tua conta está bloqueada
+      not_found_in_database: "%{authentication_keys} ou palavra-passe não válida."
+      timeout: A tua sessão expirou. Por favor, entra de novo para continuares.
+      unauthenticated: Precisas de entrar na tua conta ou registares-te antes de continuar.
+      unconfirmed: Tens de confirmar o teu endereço de email antes de continuar.
     mailer:
       confirmation_instructions:
-        subject: 'Mastodon: Instruções de confirmação'
+        subject: 'Mastodon: Instruções de confirmação %{instance}'
       password_change:
-        subject: 'Mastodon: Password nova'
+        subject: 'Mastodon: Nova palavra-passe'
       reset_password_instructions:
-        subject: 'Mastodon: Instruções para editar a password'
+        subject: 'Mastodon: Instruções para editar a palavra-passe'
       unlock_instructions:
-        subject: 'Mastodon: Instruções para desproteger a sua conta'
+        subject: 'Mastodon: Instruções para desbloquear a tua conta'
     omniauth_callbacks:
-      failure: Could not authenticate you from %{kind} because "%{reason}".
-      success: Successfully authenticated from %{kind} account.
+      failure: Não foi possível autenticar %{kind} porque "%{reason}".
+      success: Autenticado com sucesso na conta %{kind}.
     passwords:
-      no_token: Você não pode aceder a esta página sem ter vindo de um email para mudar a password. Se este for o case, por favor faça questão de verificar que usou o URL no email.
-      send_instructions: Irá receber um email com instruções em como mudar a sua password dentro de algns minutos.
-      send_paranoid_instructions: Se  seu endereço de email existe na nossa base de dados, irá receber um link para recuperar a sua password dentro de alguns minutos.
-      updated: A sua password foi alterada. A sua sessão está aberta.
-      updated_not_active: A sua password foi alterada.
+      no_token: Não pode aceder a esta página se não vier através do link enviado por email para alteração da sua palavra-passe. Se usaste esse link para chegar aqui, por favor verifica que o endereço URL actual é o mesmo do que foi enviado no email.
+      send_instructions: Vais receber um email com instruções para alterar a palavra-passe dentro de algns minutos.
+      send_paranoid_instructions: Se o teu endereço de email existe na nossa base de dados, vais receber um link para recuperar a palavra-passe dentro de alguns minutos.
+      updated: A tua palavra-passe foi alterada. Estás agora autenticado na tua conta.
+      updated_not_active: A tua palavra-passe foi alterada.
     registrations:
-      destroyed: Adeus! A sua conta foi cancelada. Esperamos vê-lo em breve.
-      signed_up: Bem vindo! A sua conta foi registada com sucesso.
-      signed_up_but_inactive: A sua conta foi registada. No entanto, não abrimos a sua sessão porque a sua conta ainda não foi ativada.
-      signed_up_but_locked: A sua conta foi registada. No entanto, não abrimos a sua sessão porque a sua conta está protegida.
-      signed_up_but_unconfirmed: Uma mensagem com um link de confirmação foi enviada para o seu email. Por favor siga o link para ativar a sua conta.
-      update_needs_confirmation: Você mudou o seu endereço de email ou password, mas é necessário confirmar a mudança. Por favor siga o link que foi enviado para o seu novo endereço de email.
-      updated: A sua conta foi alterada com sucesso.
+      destroyed: Adeus! A tua conta foi cancelada. Esperamos ver-te em breve.
+      signed_up: Bem-vindo! A tua conta foi registada com sucesso.
+      signed_up_but_inactive: A tua conta foi registada. No entanto ainda não está activa.
+      signed_up_but_locked: A tua conta foi registada. No entanto está bloqueada.
+      signed_up_but_unconfirmed: Uma mensagem com um link de confirmação foi enviada para o teu email. Por favor segue esse link para activar a tua conta.
+      update_needs_confirmation: Alteraste o teu endereço de email ou palavra-passe, mas é necessário confirmar essa alteração. Por favor vai ao teu email e segue link que te enviámos.
+      updated: A tua conta foi actualizada com sucesso.
     sessions:
-      already_signed_out: Sessão fechada.
+      already_signed_out: Sessão encerrada.
       signed_in: Sessão iniciada.
-      signed_out: Sessão fechada.
+      signed_out: Sessão encerrada.
     unlocks:
-      send_instructions: Irá receber um email com instruções para desproteger a sua conta dentro de alguns minutos.
-      send_paranoid_instructions: Se a sua conta existe, irá receber um email com instruções a detalhar como a desproteger dentro de alguns minutos.
-      unlocked: A sua conta foi desprotegida. Por favor inicie sessão para continuar.
+      send_instructions: Vais receber um email com instruções para desbloquear a tua conta dentro de alguns minutos.
+      send_paranoid_instructions: Se a tua conta existe, vais receber um email com instruções a detalhar como a desbloquear dentro de alguns minutos.
+      unlocked: A sua conta foi desbloqueada. Por favor inica uma nova sessão para continuar.
   errors:
     messages:
-      already_confirmed: já foi confirmado, por favor tente iniciar sessão
-      confirmation_period_expired: tem de ser confirmado dentro de %{period}, por favor tente outra vez
+      already_confirmed: já confirmado, por favor tente iniciar sessão
+      confirmation_period_expired: tem de ser confirmado durante %{period}, por favor tenta outra vez
       expired: expirou, por favor tente outra vez
       not_found: não encontrado
-      not_locked: não está protegido
+      not_locked: não estava bloqueada
       not_saved:
         one: '1 erro impediu este %{resource} de ser guardado:'
         other: "%{count} erros impediram este %{resource} de ser guardado:"
diff --git a/config/locales/doorkeeper.nl.yml b/config/locales/doorkeeper.nl.yml
index 91e62dc0c..9edbb8c9f 100644
--- a/config/locales/doorkeeper.nl.yml
+++ b/config/locales/doorkeeper.nl.yml
@@ -4,7 +4,7 @@ nl:
     attributes:
       doorkeeper/application:
         name: Naam
-        redirect_uri: Redirect URI
+        redirect_uri: Redirect-URI
         scopes: Scopes
     errors:
       models:
@@ -26,15 +26,15 @@ nl:
       confirmations:
         destroy: Weet je het zeker?
       edit:
-        title: Bewerk applicatie
+        title: Applicatie bewerken
       form:
         error: Oops! Controleer het formulier op fouten
       help:
         native_redirect_uri: Gebruik %{native_redirect_uri} voor lokale tests
         redirect_uri: 'Gebruik één regel per URI. '
-        scopes: Scheid scopes met spaties. Laat leeg om de standaard scopes te gebruiken.
+        scopes: Scopes met spaties van elkaar scheiden. Laat leeg om de standaardscopes te gebruiken.
       index:
-        callback_url: Callback URL
+        callback_url: Callback-URL
         name: Naam
         new: Nieuwe applicatie
         title: Jouw applicaties
@@ -42,8 +42,8 @@ nl:
         title: Nieuwe applicatie
       show:
         actions: Acties
-        application_id: Applicatie Id
-        callback_urls: Callback urls
+        application_id: Applicatie-ID
+        callback_urls: Callback-URL's
         scopes: Scopes
         secret: Secret
         title: 'Applicatie: %{name}'
@@ -58,7 +58,7 @@ nl:
         prompt: "%{client_name} autoriseren om uw account te gebruiken?"
         title: Autorisatie vereist
       show:
-        title: Autorisatie code
+        title: Autorisatie-code
     authorized_applications:
       buttons:
         revoke: Intrekken
@@ -71,24 +71,24 @@ nl:
         title: Jouw geautoriseerde applicaties
     errors:
       messages:
-        access_denied: De resource eigenaar of autorisatie-server weigerde het verzoek.
-        credential_flow_not_configured: Resource Owner Password Credentials flow failed due to Doorkeeper.configure.resource_owner_from_credentials being unconfigured.
-        invalid_client: Client verificatie is mislukt door onbekende klant, geen client authenticatie opgegeven, of een niet-ondersteunde authenticatie methode.
-        invalid_grant: De verstrekte autorisatie is ongeldig, verlopen, ingetrokken, komt niet overeen met de redirect uri die is opgegeven, of werd uitgegeven aan een andere klant.
-        invalid_redirect_uri: De opgegeven redirect uri is niet geldig.
-        invalid_request: Het verzoek mist een vereiste parameter, bevat een niet-ondersteunde parameter waarde of is anderszins onjuist.
-        invalid_resource_owner: De verstrekte resource eigenaar gegevens zijn niet geldig of de resource eigenaar kan niet worden gevonden
-        invalid_scope: De opgevraagde scope is niet geldig, onbekend of onjuist.
+        access_denied: De resource-eigenaar of autorisatie-server weigerde het verzoek.
+        credential_flow_not_configured: De wachtwoordgegevens-flow van de resource-eigenaar is mislukt omdat Doorkeeper.configure.resource_owner_from_credentials niet is ingesteld.
+        invalid_client: Clientverificatie is mislukt door een onbekende client, ontbrekende client-authenticatie of een niet ondersteunde authenticatie-methode.
+        invalid_grant: De verstrekte autorisatie is ongeldig, verlopen, ingetrokken, komt niet overeen met de redirect-URI die is opgegeven of werd uitgegeven aan een andere client.
+        invalid_redirect_uri: De opgegeven redirect-URI is ongeldig.
+        invalid_request: Het verzoek mist een vereiste parameter, bevat een niet ondersteunde parameterwaarde of is anderszins onjuist.
+        invalid_resource_owner: De verstrekte resource-eigenaargegevens zijn ogeldig of de resource-eigenaar kan niet worden gevonden.
+        invalid_scope: De opgevraagde scope is ongeldig, onbekend of onjuist.
         invalid_token:
-          expired: Het toegangstoken is verlopen
-          revoked: Het toegangstoken is geweigerd
-          unknown: Het toegangstoken is ongeldig
-        resource_owner_authenticator_not_configured: Resource Owner find failed due to Doorkeeper.configure.resource_owner_authenticator being unconfiged.
-        server_error: De autorisatieserver is een onverwachte voorwaarde tegengekomen die het verzoek verhinderd.
+          expired: Het toegangssleutel is verlopen
+          revoked: Het toegangssleutel is geweigerd
+          unknown: Het toegangssleutel is ongeldig
+        resource_owner_authenticator_not_configured: Het opzoeken van de resource-eigenaar is mislukt omdat Doorkeeper.configure.resource_owner_authenticator niet is ingesteld.
+        server_error: De autorisatieserver is is een onverwachte situatie tegengekomen die het verzoek verhinderde.
         temporarily_unavailable: De autorisatieserver is momenteel niet in staat het verzoek te behandelen als gevolg van een tijdelijke overbelasting of onderhoud aan de server.
-        unauthorized_client: De client is niet bevoegd om dit verzoek met deze methode uit te voeren.
-        unsupported_grant_type: Het type autorisatie is niet ondersteund door de autorisatieserver
-        unsupported_response_type: De autorisatieserver ondersteund dit response type niet
+        unauthorized_client: De client is niet bevoegd om dit verzoek op deze manier uit te voeren.
+        unsupported_grant_type: Het type autorisatie wordt niet door de autorisatieserver ondersteund 
+        unsupported_response_type: De autorisatieserver ondersteund dit antwoordtype niet
     flash:
       applications:
         create:
@@ -105,10 +105,10 @@ nl:
         nav:
           applications: Applicaties
           home: Home
-          oauth2_provider: OAuth2 Provider
+          oauth2_provider: OAuth2-provider
       application:
-        title: OAuth autorisatie vereist
+        title: OAuth-autorisatie vereist
     scopes:
-      follow: volg, blokkeer, deblokkeer en stop volgen accounts
-      read: lees je accountgegevens
-      write: plaatsen namens jou
+      follow: volg, blokkeer, deblokkeer en stop het volgen van accounts
+      read: lees jouw accountgegevens
+      write: namens jou plaatsen
diff --git a/config/locales/doorkeeper.pt-BR.yml b/config/locales/doorkeeper.pt-BR.yml
new file mode 100644
index 000000000..85ea3bfcc
--- /dev/null
+++ b/config/locales/doorkeeper.pt-BR.yml
@@ -0,0 +1,112 @@
+---
+pt-BR:
+  activerecord:
+    attributes:
+      doorkeeper/application:
+        name: Nome
+        redirect_uri: Redirect URI
+    errors:
+      models:
+        doorkeeper/application:
+          attributes:
+            redirect_uri:
+              fragment_present: não pode conter um fragmento.
+              invalid_uri: tem de ser um URI válido.
+              relative_uri: tem de ser um URI absoluto.
+              secured_uri: tem de ser um HTTPS/SSL URI.
+  doorkeeper:
+    applications:
+      buttons:
+        authorize: Autorizar
+        cancel: Cancelar
+        destroy: Destruir
+        edit: Editar
+        submit: Submeter
+      confirmations:
+        destroy: Tem a certeza?
+      edit:
+        title: Editar aplicação
+      form:
+        error: Oops! Verifique que o formulário não tem erros
+      help:
+        native_redirect_uri: Use %{native_redirect_uri} para testes locais
+        redirect_uri: Utilize uma linha por URI
+        scopes: Separate scopes with spaces. Leave blank to use the default scopes.
+      index:
+        callback_url: Callback URL
+        name: Nome
+        new: Nova Aplicação
+        title: As suas aplicações
+      new:
+        title: Nova aplicação
+      show:
+        actions: Ações
+        application_id: Id de Aplicação
+        callback_urls: Callback urls
+        scopes: Scopes
+        secret: Segredo
+        title: 'Aplicação: %{name}'
+    authorizations:
+      buttons:
+        authorize: Autorize
+        deny: Não autorize
+      error:
+        title: Ocorreu um erro
+      new:
+        able_to: Vai poder
+        prompt: Aplicação %{client_name} requisita acesso à sua conta
+        title: Autorização é necessária
+      show:
+        title: Código de autorização
+    authorized_applications:
+      buttons:
+        revoke: Revogar
+      confirmations:
+        revoke: Tem a certeza?
+      index:
+        application: Aplicação
+        created_at: Criada em
+        date_format: "%Y-%m-%d %H:%M:%S"
+        title: As suas aplicações autorizadas
+    errors:
+      messages:
+        access_denied: The resource owner or authorization server denied the request.
+        credential_flow_not_configured: Resource Owner Password Credentials flow failed due to Doorkeeper.configure.resource_owner_from_credentials being unconfigured.
+        invalid_client: Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.
+        invalid_grant: The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.
+        invalid_redirect_uri: The redirect uri included is not valid.
+        invalid_request: The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed.
+        invalid_resource_owner: The provided resource owner credentials are not valid, or resource owner cannot be found
+        invalid_scope: The requested scope is invalid, unknown, or malformed.
+        invalid_token:
+          expired: O token de acesso expirou
+          revoked: O token de acesso foi revogado
+          unknown: O token de acesso é inválido
+        resource_owner_authenticator_not_configured: Resource Owner find failed due to Doorkeeper.configure.resource_owner_authenticator being unconfiged.
+        server_error: The authorization server encountered an unexpected condition which prevented it from fulfilling the request.
+        temporarily_unavailable: The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.
+        unauthorized_client: The client is not authorized to perform this request using this method.
+        unsupported_grant_type: The authorization grant type is not supported by the authorization server.
+        unsupported_response_type: The authorization server does not support this response type.
+    flash:
+      applications:
+        create:
+          notice: Aplicação criada.
+        destroy:
+          notice: Aplicação eliminada.
+        update:
+          notice: Aplicação alterada.
+      authorized_applications:
+        destroy:
+          notice: Aplicação revogada.
+    layouts:
+      admin:
+        nav:
+          applications: Aplicações
+          oauth2_provider: OAuth2 Provider
+      application:
+        title: Autorização OAuth necessária
+    scopes:
+      follow: siga, bloqueie, desbloqueie, e deixe de seguir contas
+      read: tenha acesso aos dados da sua conta
+      write: públique por si
diff --git a/config/locales/doorkeeper.pt.yml b/config/locales/doorkeeper.pt.yml
index 2709856e8..87e01ba94 100644
--- a/config/locales/doorkeeper.pt.yml
+++ b/config/locales/doorkeeper.pt.yml
@@ -23,20 +23,20 @@ pt:
         edit: Editar
         submit: Submeter
       confirmations:
-        destroy: Tem a certeza?
+        destroy: Tens a certeza?
       edit:
         title: Editar aplicação
       form:
-        error: Oops! Verifique que o formulário não tem erros
+        error: Oops! Verifica que o formulário não tem erros
       help:
-        native_redirect_uri: Use %{native_redirect_uri} para testes locais
-        redirect_uri: Utilize uma linha por URI
+        native_redirect_uri: Usa %{native_redirect_uri} para testes locais
+        redirect_uri: Utiliza uma linha por URI
         scopes: Separate scopes with spaces. Leave blank to use the default scopes.
       index:
         callback_url: Callback URL
         name: Nome
         new: Nova Aplicação
-        title: As suas aplicações
+        title: As tuas aplicações
       new:
         title: Nova aplicação
       show:
@@ -54,7 +54,7 @@ pt:
         title: Ocorreu um erro
       new:
         able_to: Vai poder
-        prompt: Aplicação %{client_name} requisita acesso à sua conta
+        prompt: Aplicação %{client_name} pede acesso à tua conta
         title: Autorização é necessária
       show:
         title: Código de autorização
@@ -62,12 +62,12 @@ pt:
       buttons:
         revoke: Revogar
       confirmations:
-        revoke: Tem a certeza?
+        revoke: Tens a certeza?
       index:
         application: Aplicação
         created_at: Criada em
         date_format: "%Y-%m-%d %H:%M:%S"
-        title: As suas aplicações autorizadas
+        title: As tuas aplicações autorizadas
     errors:
       messages:
         access_denied: The resource owner or authorization server denied the request.
@@ -107,6 +107,6 @@ pt:
       application:
         title: Autorização OAuth necessária
     scopes:
-      follow: siga, bloqueie, desbloqueie, e deixe de seguir contas
-      read: tenha acesso aos dados da sua conta
-      write: públique por si
\ No newline at end of file
+      follow: siga, bloqueie, desbloqueie, e deixa de seguir contas
+      read: tenha acesso aos dados da tua conta
+      write: públique por ti
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index ec38b411a..4644d1bad 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -71,6 +71,7 @@ fr:
       profile_url: URL du profil
       public: Public
       push_subscription_expires: Expiration de l'abonnement PuSH
+      reset_password: Réinitialiser le mot de passe
       salmon_url: URL Salmon
       silence: Rendre muet
       statuses: Statuts
@@ -81,6 +82,8 @@ fr:
       web: Web
     domain_blocks:
       add_new: Ajouter
+      created_msg: Le blocage de domaine est désormais activé
+      destroyed_msg: Le blocage de domaine a été désactivé
       domain: Domaine
       new:
         create: Créer le blocage
@@ -90,8 +93,23 @@ fr:
           silence: Muet
           suspend: Suspendre
         title: Nouveau blocage de domaine
+      reject_media: Fichiers media rejetés
+      reject_media_hint: Supprime localement les fichiers media stockés et refuse d'en télécharger ultérieurement. Ne concerne pas les suspensions.
+      severities:
+        silence: Rendre muet
+        suspend: Suspendre
       severity: Séverité
+      show:
+        affected_accounts:
+          one: Un compte affecté dans la base de données
+          other: "%{count} comptes affectés dans la base de données"
+        retroactive:
+          silence: Annuler le silence sur tous les comptes existants pour ce domaine
+          suspend: Annuler la suspension sur tous les comptes existants pour ce domaine
+        title: Annuler le blocage de domaine pour %{domain}
+        undo: Annuler
       title: Blocage de domaines
+      undo: Annuler
     pubsubhubbub:
       callback_url: URL de rappel
       confirmed: Confirmé
@@ -192,6 +210,7 @@ fr:
     blocks: Vous bloquez
     csv: CSV
     follows: Vous suivez
+    mutes: Vous faites taire
     storage: Médias stockés
   generic:
     changes_saved_msg: Les modifications ont été enregistrées avec succès !
@@ -206,8 +225,13 @@ fr:
     types:
       blocking: Liste d'utilisateurs⋅trices bloqué⋅es
       following: Liste d'utilisateurs⋅trices suivi⋅es
+      muting: Liste d'utilisateurs⋅trices que vous faites taire
     upload: Importer
   landing_strip_html: <strong>%{name}</strong> utilise <strong>%{domain}</strong>. Vous pouvez le/la suivre et interagir si vous possédez un compte quelque part dans le "fediverse". Si ce n'est pas le cas, vous pouvez <a href="%{sign_up_path}">en créer un ici</a>.
+  media_attachments:
+    validations:
+      images_and_video: Impossible de joindre une vidéo à un statuts contenant déjà des images
+      too_many: Impossible de joindre plus de 4 fichiers
   notification_mailer:
     digest:
       body: 'Voici ce que vous avez raté sur ${instance} depuis votre dernière visite (%{}) :'
@@ -266,10 +290,19 @@ fr:
     formats:
       default: "%d %b %Y, %H:%M"
   two_factor_auth:
+    code_hint: Entrez le code généré par votre application pour confirmer
     description_html: Si vous activez <strong>l'identification à deux facteurs</strong>, vous devrez être en possession de votre téléphone afin de générer un code de connexion.
     disable: Désactiver
     enable: Activer
+    enabled_success: Identification à deux facteurs activée avec succès
+    generate_recovery_codes: Générer les codes de récupération
     instructions_html: "<strong>Scannez ce QR code grâce à Google Authenticator, Authy ou une application similaire sur votre téléphone</strong>. Désormais, cette application générera des jetons que vous devrez saisir à chaque connexion."
+    lost_recovery_codes: Les codes de récupération vous permettent de retrouver les accès à votre comptre si vous perdez votre téléphone. Si vous perdez vos codes de récupération, vous pouvez les générer à nouveau ici. Vos anciens codes de récupération seront invalidés.
+    manual_instructions: "Si vous ne pouvez pas scanner ce QR code et devez l'entrer manuellement, voici le secret en clair :"
+    recovery_codes_regenerated: Codes de récupération régénérés avec succès
+    recovery_instructions: Si vous perdez l'accès à votre téléphone, vous pouvez utiliser un des code de récupération ci-dessous pour récupérer l'accès à votre compte. Conservez les codes de récupération en toute sécurité, par exemple, en les imprimant et en les stockant avec vos autres documents importants.
+    setup: Installer
+    wrong_code: Les codes entrés sont incorrects ! L'heure du serveur et celle de votre appareil sont-elles correctes ?
   users:
     invalid_email: L'adresse courriel est invalide
     invalid_otp_token: Le code d'authentification à deux facteurs est invalide
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index c9885b5f6..96fc514da 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -94,7 +94,7 @@ ja:
           suspend: 停止
         title: 新規ドメインブロック
       reject_media: メディアファイルを拒否
-      reject_media_hint: ローカルに保村されたメディアファイルを削除し、今後のダウンロードを拒否します。停止とは無関係です。
+      reject_media_hint: ローカルに保存されたメディアファイルを削除し、今後のダウンロードを拒否します。停止とは無関係です。
       severities:
         silence: サイレンス
         suspend: 停止
@@ -272,9 +272,9 @@ ja:
     over_character_limit: 上限は %{max}文字までです
     show_more: もっと見る
     visibilities:
-      private: Private - フォロワーだけに見せる
-      public: Public - 全体に公開する
-      unlisted: Unlisted - トゥートは公開するが、公開タイムラインには表示しない
+      private: 非公開 - フォロワーだけに公開
+      public: 公開 - 公開タイムラインに投稿する
+      unlisted: 未収載 - トゥートは公開するが、公開タイムラインには表示しない
   stream_entries:
     click_to_show: クリックして表示
     reblogged: ブーストされました
@@ -288,12 +288,12 @@ ja:
     disable: 無効
     enable: 有効
     enabled_success: 二段階認証が有効になりました
-    generate_recovery_codes: 復元コードを生成
+    generate_recovery_codes: リカバリーコードを生成
     instructions_html: "<strong>Google Authenticatorか、もしくはほかのTOTPアプリでこのQRコードをスキャンしてください。</strong>これ以降、ログインするときはそのアプリで生成されるコードが必要になります。"
-    lost_recovery_codes: リカバリコードを使用すると携帯電話を紛失した場合でもアカウントにアクセスできるようになります。 リカバリーコードを紛失した場合もここで再生成することができますが、古いリカバリコードは無効になります。
+    lost_recovery_codes: リカバリーコードを使用すると携帯電話を紛失した場合でもアカウントにアクセスできるようになります。 リカバリーコードを紛失した場合もここで再生成することができますが、古いリカバリーコードは無効になります。
     manual_instructions: 'QRコードがスキャンできず、手動での登録を希望の場合はこのシークレットコードを利用してください。:'
     recovery_codes_regenerated: リカバリーコードが再生成されました。
-    recovery_instructions: 携帯電話を紛失した場合、以下の内どれかのリカバリコードを使用してアカウントへアクセスすることができます。 リカバリコードは印刷して安全に保管してください。
+    recovery_instructions: 携帯電話を紛失した場合、以下の内どれかのリカバリーコードを使用してアカウントへアクセスすることができます。 リカバリーコードは印刷して安全に保管してください。
     setup: 初期設定
     wrong_code: コードが間違っています。サーバー上の時間とデバイス上の時間が一致していることを確認してください。
   users:
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index 577b32454..8471743a5 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -1,34 +1,34 @@
 ---
 nl:
   about:
-    about_mastodon: Mastodon is een <em>vrij, gratis en open-source</em> sociaal netwerk. Een <em>gedecentraliseerd</em> alternatief voor commerciële platforms, het voorkomt de risico's van een enkel bedrijf dat jouw communicatie monopoliseert. Kies een server die je vertrouwt &mdash; welke je ook kiest, je kunt met iedere ander communiceren. Iedereen kan een eigen Mastodon-server draaien en naadloos deelnemen in het <em>sociale netwerk</em>.
+    about_mastodon: Mastodon is een <em>vrij, gratis en open-source</em> sociaal netwerk. Een <em>gedecentraliseerd</em> alternatief voor commerciële platforms. Het voorkomt de risico's van een enkel bedrijf dat jouw communicatie monopoliseert. Kies een server die je vertrouwt &mdash; welke je ook kiest, je kunt met elke andere server communiceren. Iedereen kan een eigen Mastodon-server draaien en naadloos deelnemen in het <em>sociale netwerk</em>.
     about_this: Over deze server
     apps: Apps
-    business_email: 'Zakelijk e-mailadres:'
+    business_email: 'E-mailadres:'
     closed_registrations: Registreren op deze server is momenteel uitgeschakeld.
     contact: Contact
     description_headline: Wat is %{domain}?
     domain_count_after: andere servers
     domain_count_before: Verbonden met
     features:
-      api: Open API voor apps en services
-      blocks: Rijke blokkeer- en dempingshulpmiddelen
+      api: Open API voor apps en diensten
+      blocks: Uitgebreide blokkeer- en negeerhulpmiddelen
       characters: 500 tekens per bericht
       chronology: Tijdlijnen zijn chronologisch
-      ethics: 'Ethisch design: geen ads, geen spionage'
+      ethics: 'Ethisch design: geen advertenties, geen spionage'
       gifv: GIFV-sets en korte video's
-      privacy: Granulaire privacyinstellingen per bericht
+      privacy: Nauwkeurige privacyinstellingen per toot (bericht)
       public: Openbare tijdlijnen
     features_headline: Wat maakt Mastodon anders
     get_started: Beginnen
     links: Links
     other_instances: Andere servers
-    source_code: Source code
-    status_count_after: statussen
-    status_count_before: Wie schreef
+    source_code: Broncode
+    status_count_after: toots
+    status_count_before: Zij schreven
     terms: Voorwaarden
     user_count_after: gebruikers
-    user_count_before: Thuis naar
+    user_count_before: Thuisbasis van
   accounts:
     follow: Volgen
     followers: Volgers
@@ -37,7 +37,7 @@ nl:
     people_followed_by: Mensen die %{name} volgt
     people_who_follow: Mensen die %{name} volgen
     posts: Berichten
-    remote_follow: Externe volg
+    remote_follow: Extern volgen
     unfollow: Ontvolgen
   application_mailer:
     settings: 'E-mailvoorkeuren wijzigen: %{link}'
@@ -58,7 +58,7 @@ nl:
   authorize_follow:
     error: Helaas, er is een fout opgetreden bij het opzoeken van de externe account
     follow: Volgen
-    prompt_html: 'Je (<strong>%{self}</strong>) hebt volgen aangevraagd:'
+    prompt_html: 'Je (<strong>%{self}</strong>) hebt toestemming gevraagd om iemand te mogen volgen:'
     title: Volg %{acct}
   datetime:
     distance_in_words:
@@ -66,84 +66,86 @@ nl:
       about_x_months: "%{count}ma"
       about_x_years: "%{count}j"
       almost_x_years: "%{count}j"
-      half_a_minute: Net
+      half_a_minute: Zojuist
       less_than_x_minutes: "%{count}m"
-      less_than_x_seconds: Net
+      less_than_x_seconds: Zojuist
       over_x_years: "%{count}j"
       x_days: "%{count}d"
       x_minutes: "%{count}m"
       x_months: "%{count}ma"
       x_seconds: "%{count}s"
   exports:
-    blocks: Je blokkeert
+    blocks: Jij blokkeert
     csv: CSV
-    follows: Je volgt
+    follows: Jij volgt
+    mutes: Jij negeert
     storage: Mediaopslag
   generic:
     changes_saved_msg: Wijzigingen succesvol opgeslagen!
     powered_by: mogelijk gemaakt door %{link}
-    save_changes: Wijziginen opslaan
+    save_changes: Wijzigingen opslaan
     validation_errors:
       one: Er is iets niet helemaal goed! Bekijk onderstaande fout
       other: Er is iets niet helemaal goed! Bekijk onderstaande %{count} fouten
   imports:
-    preface: Je kunt bepaalde gegevens, zoals de mensen die je volgt of blokkeert, importeren voor je account op deze server, als ze zijn geëxporteerd op een andere server.
-    success: Je gegevens zijn succesvol geüpload en worden binnenkort verwerkt
+    preface: Je kunt bepaalde gegevens, zoals de mensen die jij volgt of hebt geblokkeerd, naar jouw account op deze server importeren. Je moet deze gegevens wel eerst op de oorspronkelijke server exporteren.
+    success: Jouw gegevens zijn succesvol geüpload en worden binnenkort verwerkt
     types:
       blocking: Blokkeerlijst
       following: Volglijst
+      muting: Negeerlijst
     upload: Uploaden
-  landing_strip_html: <strong>%{name}</strong> is een gebruiker op <strong>%{domain}</strong>. Je kunt deze volgen of ermee interacteren als je ergens in deze fediverse een account hebt. Als je dat niet hebt, kun je je <a href="%{sign_up_path}">hier aanmelden</a>.
+  landing_strip_html: <strong>%{name}</strong> is een gebruiker op <strong>%{domain}</strong>. Je kunt deze volgen en ermee communiceren als je ergens in deze fediverse een account hebt. Als je dat niet hebt, kun je je <a href="%{sign_up_path}">hier aanmelden</a>.
   notification_mailer:
     digest:
-      body: 'Hier is een korte samenvatting van wat je hebt gemist op %{instance} sinds je laatste bezoek op %{since}:'
-      mention: "%{name} vermeldde je in:"
+      body: 'Hier is een korte samenvatting van wat je hebt gemist op %{instance} sinds jouw laatste bezoek op %{since}:'
+      mention: "%{name} vermeldde jou in:"
       new_followers_summary:
-        one: Je hebt een nieuwe volger! Hoera!
-        other: Je hebt %{count} nieuwe volgers! Prachtig!
+        one: Jij hebt een nieuwe volger! Hoera!
+        other: Jij hebt %{count} nieuwe volgers! Prachtig!
       subject:
-        one: "1 nieuwe melding sinds je laatste bezoek \U0001F418"
-        other: "%{count} nieuwe meldingen sinds je laatste bezoek \U0001F418"
+        one: "1 nieuwe melding sinds jouw laatste bezoek \U0001F418"
+        other: "%{count} nieuwe meldingen sinds jouw laatste bezoek \U0001F418"
     favourite:
-      body: 'Je status werd door  %{name} als favoriet gemarkeerd:'
-      subject: "%{name} markeerde je status als favoriet"
+      body: 'Jouw toot werd door %{name} als favoriet gemarkeerd:'
+      subject: "%{name} markeerde jouw toot als favoriet"
     follow:
-      body: "%{name} volgt je nu!"
-      subject: "%{name} volgt je nu"
+      body: "%{name} volgt jou nu!"
+      subject: "%{name} volgt jou nu"
     follow_request:
-      body: "%{name} wil je graag volgen"
+      body: "%{name} wil jou graag volgen"
       subject: 'Volgen in afwachting: %{name}'
     mention:
-      body: 'Je bent door %{name} vermeld in:'
-      subject: Je bent vermeld door %{name}
+      body: 'Jij bent door %{name} vermeld in:'
+      subject: Jij bent vermeld door %{name}
     reblog:
-      body: 'Je status werd geboost door %{name}:'
-      subject: "%{name} booste je status"
+      body: 'Jouw toot werd door %{name} geboost:'
+      subject: "%{name} booste jouw toot"
   pagination:
     next: Volgende
     prev: Vorige
   remote_follow:
-    acct: Geef je gebruikersnaam@domein op waarvandaan je wilt volgen
-    missing_resource: Kon vereiste doorverwijzings-URL voor je account niet vinden
+    acct: Geef jouw account@domein.tld op waarvandaan je wilt volgen
+    missing_resource: Kon vereiste doorverwijzings-URL voor jouw account niet vinden
     proceed: Ga door om te volgen
-    prompt: 'Je gaat volgen:'
+    prompt: 'Jij gaat volgen:'
   settings:
     authorized_apps: Geautoriseerde
     back: Terug naar Mastodon
     edit_profile: Profiel bewerken
-    export: Gegevensexport
+    export: Export
     import: Import
     preferences: Voorkeuren
     settings: Instellingen
-    two_factor_auth: Twee-factorauthenticatie
+    two_factor_auth: Tweestapsverificatie
   statuses:
     open_in_web: Openen in web
-    over_character_limit: Tekenlimiet van %{max} overschreden
+    over_character_limit: Limiet van %{max} tekens overschreden
     show_more: Toon meer
     visibilities:
       private: Alleen aan volgers tonen
       public: Openbaar
-      unlisted: Openbaar, maar niet tonen op openbare tijdlijn
+      unlisted: Openbaar, maar niet op de openbare tijdlijn tonen
   stream_entries:
     click_to_show: Klik om te tonen
     reblogged: boostte
@@ -152,10 +154,59 @@ nl:
     formats:
       default: "%b %d, %J, %U:%M"
   two_factor_auth:
-    description_html: Als je <strong>twee-factorauthenticatie</strong> instelt, kun je alleen aanmelden als je je mobiele telefoon bij je hebt, waarmee je de in te voeren tokens genereert.
+    description_html: Na het instellen van <strong>tweestapsverificatie</strong>, kun jij je alleen aanmelden als je jouw mobiele telefoon bij je hebt. Hiermee genereer je namelijk de in te voeren aanmeldcode.
     disable: Uitschakelen
     enable: Inschakelen
-    instructions_html: "<strong>Scan deze QR-code in Google Authenticator of een soortgelijke app op je mobiele telefoon</strong>. Van nu af aan creëert deze app tokens die je bij aanmelden moet invoeren."
+    instructions_html: "<strong>Scan deze QR-code in Google Authenticator of een soortgelijke app op jouw mobiele telefoon</strong>. Van nu af aan genereert deze app aanmeldcodes die je bij het aanmelden moet invoeren."
   users:
-    invalid_email: Het e-mailadres is ongeldig
-    invalid_otp_token: Ongeldige twee-factorcode
+    invalid_email: E-mailadres is ongeldig
+    invalid_otp_token: Ongeldige tweestaps-aanmeldcode
+  errors:
+      404: De pagina waarnaar jij op zoek bent bestaat niet.
+      410: De pagina waarnaar jij op zoek bent bestaat niet meer.
+      422:
+        title: Veiligheidsverificatie mislukt
+        content: Veiligheidsverificatie mislukt. Blokkeer je toevallig cookies?
+  admin.reports:
+    title: Gerapporteerde toots
+    status: Toot
+    unresolved: Onopgelost
+    resolved: Opgelost
+    id: ID
+    target: Target
+    reported_by: Gerapporteerd door
+    comment:
+      label: Opmerking
+      none: Geen
+    view: Weergeven
+    report: 'Gerapporteerde toot #%{id}'
+    delete: Verwijderen
+    reported_account: Gerapporteerde account
+    reported_by: Gerapporteerd door
+    silence_account: Account stilzwijgen
+    suspend_account: Account blokkeren
+    mark_as_resolved: Markeer als opgelost
+  admin:
+    settings:
+      title: Server-instellingen
+      setting: Instelling
+      click_to_edit: Klik om te bewerken
+      contact_information:
+        label: Contactgegevens
+        username: Vul een gebruikersnaam in
+        email: Vul een openbaar gebruikt e-mailadres in
+      site_title: Naam Mastodon-server
+      site_description:
+        title: Omschrijving Mastodon-server
+        desc_html: "Dit wordt als een alinea op de voorpagina getoond en gebruikt als meta-tag in de paginabron.<br>Je kan HTML gebruiken, zoals <code>&lt;a&gt;</code> en <code>&lt;em&gt;</code>."
+      site_description_extended:
+        title: Uitgebreide omschrijving Mastodon-server
+        desc_html: "Wordt op de uitgebreide informatiepagina weergegeven<br>Je kan ook hier HTML gebruiken"
+      registrations:
+        open:
+          title: Open registratie
+          enabled: Ingeschakeld
+          disabled: Uitgeschakeld
+        closed_message:
+          title: Bericht wanneer registratie is uitgeschakeld
+          desc_html: "Wordt op de voorpagina weergegeven wanneer registratie van nieuwe accounts is uitgeschakeld<br>En ook hier kan je HTML gebruiken"
diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml
new file mode 100644
index 000000000..269a1a99b
--- /dev/null
+++ b/config/locales/pt-BR.yml
@@ -0,0 +1,198 @@
+---
+pt-BR:
+  about:
+    about_mastodon: Mastodon é um servidor de rede social <em>grátis, e open-source</em>. Uma alternativa <em>descentralizada</em> ás plataformas comerciais, que evita o risco de uma única empresa monopolizar a sua comunicação. Escolha um servidor que você confie &mdash; qualquer um que escolher, você poderá interagir com todo o resto. Qualquer um pode ter uma instância Mastodon e assim participar na <em>rede social federada</em> sem problemas.
+    about_this: Sobre essa instância
+    apps: Aplicações
+    business_email: 'Email comercial:'
+    closed_registrations: Registros estão fechadas para essa instância.
+    contact: Contato
+    description_headline: O que é %{domain}?
+    domain_count_after: outras instâncias
+    domain_count_before: Conectado a
+    features:
+      api: Aberto para API de aplicações e serviços
+      blocks: Bloqueos e ferramentas para mudar
+      characters: 500 caracteres por post
+      chronology: Timeline são cronologicas
+      ethics: 'Design ético: sem propaganda, sem tracking'
+      gifv: GIFV e vídeos curtos
+      privacy: Granular, privacidade setada por post
+      public: Timelines públicas
+    features_headline: O que torna Mastodon diferente
+    get_started: Comece aqui
+    links: Links
+    source_code: Source code
+    other_instances: Outras instâncias
+    terms: Termos
+    user_count_after: usuários
+    user_count_before: Lugar de
+  accounts:
+    follow: Seguir
+    followers: Seguidores
+    following: Seguindo
+    nothing_here: Não há nada aqui!
+    people_followed_by: Pessoas seguidas por %{name}
+    people_who_follow: Pessoas que seguem %{name}
+    posts: Posts
+    remote_follow: Acesso remoto
+    unfollow: Unfollow
+  admin:
+    accounts:
+      are_you_sure: Você tem certeza?
+      display_name: Nome mostrado
+      domain: Domain
+      edit: Editar
+      email: E-mail
+      feed_url: URL do Feed
+      followers: Seguidores
+      follows: Seguindo
+      location:
+        all: Todos
+        local: Local
+        remote: Remoto
+        title: Local
+      media_attachments: Mídia anexadas
+      moderation:
+        all: Todos
+        silenced: Silenciado
+        suspended: Supenso
+        title: Moderação
+      most_recent_activity: Atividade mais recente
+      most_recent_ip: IP mais recente
+      not_subscribed: Não inscrito
+      order:
+        alphabetic: Alfabética
+        most_recent: Mais recente
+        title: Ordem
+      perform_full_suspension: Fazer suspensão completa
+      profile_url: URL do perfil
+      public: Público
+      push_subscription_expires: PuSH subscription expires
+      salmon_url: Salmon URL
+      silence: Silêncio
+      statuses: Status
+      title: Contas
+      undo_silenced: Desfazer silenciar
+      undo_suspension: Desfazer supensão
+      username: Usuário
+      web: Web
+    domain_blocks:
+      add_new: Adicionar nova
+      created_msg: Bloqueio do domínio está sendo processado
+      destroyed_msg: Bloqueio de domínio está sendo desfeito
+      domain: Domínio
+      new:
+        create: Criar bloqueio
+        hint: O bloqueio de dominio não vai previnir a criação de entradas no banco de dados, mas irá, retroativamente e automaticamente aplicar métodos de moderação específica nessas contas.
+        severity:
+          desc_html: "<strong>Silenciar</strong> irá fazer com que os posts dessas contas sejam invisíveis para todos que não a seguem. <strong>Supender</strong> irá remover todos o conteúdo das contas, mídia e dados do perfil."
+          silence: Silenciar
+          suspend: Suspender
+        title: Novo bloqueio de domínio
+      reject_media: Rejeitar arquivos de mídia
+      reject_media_hint: Remove localmente arquivos armazenados e rejeita fazer o download de novos no futuro. Irrelevante em suspensões.
+      severities:
+        silence: Silenciar
+        suspend: Suspender
+      severity: Severidade
+      show:
+        affected_accounts:
+          one: Uma conta no banco de dados afetada
+          other: "%{count} contas no banco de dados afetada"
+        retroactive:
+          silence: Desilenciar todas as contas existentes nesse domínio
+          suspend: Desuspender todas as contas existentes nesse domínio
+        title: Desfazer bloqueio de domínio para %{domain}
+      title: Bloqueio de domínio
+      undo: Desfazer
+    pubsubhubbub:
+      callback_url: URL de Callback
+      confirmed: Confirmado
+      expires_in: Expira em
+      last_delivery: Última entrega
+      title: PubSubHubbub
+      topic: Tópico
+    reports:
+      comment:
+        label: Commentário
+        none: None
+      delete: Deletar
+      id: ID
+      mark_as_resolved: Marque como resolvido
+      report: 'Report #%{id}'
+      reported_account: Conta reportada
+      reported_by: Reportado por
+      resolved: Resolvido
+      silence_account: Conta silenciada
+      status: Status
+      suspend_account: Conta suspensa
+      target: Target
+      title: Reports
+      unresolved: Unresolved
+      view: View
+    settings:
+      click_to_edit: Clique para editar
+      contact_information:
+        email: Entre um endereço de email público
+        label: Informação de contato
+        username: Entre com usuário
+      registrations:
+        closed_message:
+          desc_html: Mostrar na página inicial quando registros estão fecados<br/>Você pode usar tags HTML
+          title: Mensagem de registro fechados
+        open:
+          disabled: Desabilitado
+          enabled: Habilitado
+          title: Aberto para registro
+      setting: Preferências
+      site_description:
+        desc_html: Mostrar como parágrafo e usado como meta tag.<br/>Vôce pode usar tags HTML, em particular <code>&lt;a&gt;</code> e <code>&lt;em&gt;</code>.
+        title: Descrição do site
+      site_description_extended:
+        desc_html: Mostrar na página de informação extendiada <br/>Você pode usar tags HTML
+        title: Descrição extendida do site
+      site_title: Título do site
+      title: Preferências do site
+    title: Administração
+  application_mailer:
+    settings: 'Mudar preferências de email: %{link}'
+    signature: notificações Mastodon de %{instance}
+    view: 'View:'
+  applications:
+    invalid_url: URL dada é inválida
+  auth:
+    change_password: Mudar senha
+    didnt_get_confirmation: Não recebeu instruções de confirmação?
+    forgot_password: Esqueceu a senha?
+    login: Entrar
+    register: Registar
+    resend_confirmation: Reenviar instruções de confirmação
+    reset_password: Resetar senha
+    set_new_password: Editar password
+  generic:
+    changes_saved_msg: Mudanças guardadas!
+    powered_by: powered by %{link}
+    save_changes: Guardar alterações
+    validation_errors:
+      one: Algo não está correto. Por favor reveja o erro abaixo
+      other: Algo não está correto. Por favor reveja os %{count} erros abaixo
+  notification_mailer:
+    favourite:
+      body: 'O seu post foi favoritado por %{name}:'
+      subject: "%{name} favouritou o seu post"
+    follow:
+      body: "%{name} seguiu você!"
+      subject: "%{name} segue você"
+    mention:
+      body: 'Você foi mencionado por %{name} em:'
+      subject: Foi mencionado por %{name}
+    reblog:
+      body: 'O seu post foi reblogado por %{name}:'
+      subject: "%{name} reblogou o seu post"
+  pagination:
+    next: Next
+    prev: Prev
+  settings:
+    edit_profile: Editar perfil
+    preferences: Preferências
diff --git a/config/locales/pt.yml b/config/locales/pt.yml
index dc9beae7b..735bc14ba 100644
--- a/config/locales/pt.yml
+++ b/config/locales/pt.yml
@@ -1,64 +1,66 @@
 ---
 pt:
   about:
-    about_mastodon: Mastodon é um servidor de rede social <em>grátis, e open-source</em>. Uma alternativa <em>descentralizada</em> ás plataformas comerciais, que evita o risco de uma única empresa monopolizar a sua comunicação. Escolha um servidor que você confie &mdash; qualquer um que escolher, você poderá interagir com todo o resto. Qualquer um pode ter uma instância Mastodon e assim participar na <em>rede social federada</em> sem problemas.
-    about_this: Sobre essa instância
+    about_mastodon: Mastodon é uma rede social <em>grátis e em código aberto</em>. Uma alternativa <em>descentralizada</em> às plataformas comerciais, que evita o risco de uma única empresa monopolizar a tua comunicação. Escolhe um servidor que confies, não importa qual, pois vais poder comunicar com todos os outros. Qualquer um pode criar uma instância Mastodon e participar nesta <em>rede social</em>.
+    about_this: Sobre esta instância
     apps: Aplicações
     business_email: 'Email comercial:'
-    closed_registrations: Registros estão fechadas para essa instância.
-    contact: Contato
-    description_headline: O que é %{domain}?
+    closed_registrations: Novos registos estão fechados nesta instância.
+    contact: Contacto
+    description_headline: O que é o %{domain}?
     domain_count_after: outras instâncias
-    domain_count_before: Conectado a
+    domain_count_before: Ligado a
     features:
-      api: Aberto para API de aplicações e serviços
-      blocks: Bloqueos e ferramentas para mudar
+      api: API aberta para aplicações e serviços
+      blocks: Ferramentas para silenciar e bloquear
       characters: 500 caracteres por post
-      chronology: Timeline são cronologicas
-      ethics: 'Design ético: sem propaganda, sem tracking'
-      gifv: GIFV e vídeos curtos
-      privacy: Granular, privacidade setada por post
+      chronology: Timelines cronológicas
+      ethics: 'Design ético: sem públicidade ou tracking'
+      gifv: GIFV e pequenos vídeos
+      privacy: Privacidade granular por post
       public: Timelines públicas
     features_headline: O que torna Mastodon diferente
-    get_started: Comece aqui
+    get_started: Começar
     links: Links
-    source_code: Source code
     other_instances: Outras instâncias
+    source_code: Código fonte
+    status_count_after: publicações
+    status_count_before: Que fizeram
     terms: Termos
-    user_count_after: usuários
-    user_count_before: Lugar de
+    user_count_after: utilizadores
+    user_count_before: Casa para
   accounts:
     follow: Seguir
     followers: Seguidores
-    following: Seguindo
+    following: A seguir
     nothing_here: Não há nada aqui!
     people_followed_by: Pessoas seguidas por %{name}
     people_who_follow: Pessoas que seguem %{name}
     posts: Posts
-    remote_follow: Acesso remoto
-    unfollow: Unfollow
+    remote_follow: Seguir remotamente
+    unfollow: Deixar de seguir
   admin:
     accounts:
-      are_you_sure: Você tem certeza?
-      display_name: Nome mostrado
-      domain: Domain
+      are_you_sure: Tens a certeza?
+      display_name: Nome a mostrar
+      domain: Domínio
       edit: Editar
       email: E-mail
       feed_url: URL do Feed
       followers: Seguidores
-      follows: Seguindo
+      follows: A seguir
       location:
         all: Todos
         local: Local
         remote: Remoto
         title: Local
-      media_attachments: Mídia anexadas
+      media_attachments: Media anexa
       moderation:
         all: Todos
-        silenced: Silenciado
-        suspended: Supenso
+        silenced: Silenciados
+        suspended: Supensos
         title: Moderação
-      most_recent_activity: Atividade mais recente
+      most_recent_activity: Actividade mais recente
       most_recent_ip: IP mais recente
       not_subscribed: Não inscrito
       order:
@@ -69,6 +71,7 @@ pt:
       profile_url: URL do perfil
       public: Público
       push_subscription_expires: PuSH subscription expires
+      reset_password: Reset palavra-passe
       salmon_url: Salmon URL
       silence: Silêncio
       statuses: Status
@@ -78,34 +81,35 @@ pt:
       username: Usuário
       web: Web
     domain_blocks:
-      add_new: Adicionar nova
-      created_msg: Bloqueio do domínio está sendo processado
-      destroyed_msg: Bloqueio de domínio está sendo desfeito
+      add_new: Adicionar novo
+      created_msg: Bloqueio do domínio está a ser processado
+      destroyed_msg: Bloqueio de domínio está a ser removido
       domain: Domínio
       new:
         create: Criar bloqueio
-        hint: O bloqueio de dominio não vai previnir a criação de entradas no banco de dados, mas irá, retroativamente e automaticamente aplicar métodos de moderação específica nessas contas.
+        hint: O bloqueio de dominio não vai previnir a criação de entradas na base de dados, mas irá retroativamente e automaticamente aplicar métodos de moderação específica nessas contas.
         severity:
-          desc_html: "<strong>Silenciar</strong> irá fazer com que os posts dessas contas sejam invisíveis para todos que não a seguem. <strong>Supender</strong> irá remover todos o conteúdo das contas, mídia e dados do perfil."
+          desc_html: "<strong>Silenciar</strong> irá fazer com que os posts dessas contas sejam invisíveis para todos que não a seguem. <strong>Supender</strong> irá eliminar todo o conteúdo guardado dessa conta, mídia e informação de perfil."
           silence: Silenciar
           suspend: Suspender
         title: Novo bloqueio de domínio
-      reject_media: Rejeitar arquivos de mídia
-      reject_media_hint: Remove localmente arquivos armazenados e rejeita fazer o download de novos no futuro. Irrelevante em suspensões.
+      reject_media: Rejeitar ficheiros de mídia
+      reject_media_hint: Remove localmente arquivos armazenados e rejeita fazer guardar novos no futuro. Irrelevante na suspensão.
       severities:
         silence: Silenciar
         suspend: Suspender
       severity: Severidade
       show:
         affected_accounts:
-          one: Uma conta no banco de dados afetada
-          other: "%{count} contas no banco de dados afetada"
+          one: Uma conta na base de dados afectada
+          other: "%{count} contas na base de dados afectadas"
         retroactive:
-          silence: Desilenciar todas as contas existentes nesse domínio
-          suspend: Desuspender todas as contas existentes nesse domínio
-        title: Desfazer bloqueio de domínio para %{domain}
+          silence: Não silenciar todas as contas existentes nesse domínio
+          suspend: Não suspender todas as contas existentes nesse domínio
+        title: Remover o bloqueio de domínio de %{domain}
+        undo: Anular
       title: Bloqueio de domínio
-      undo: Desfazer
+      undo: Anular
     pubsubhubbub:
       callback_url: URL de Callback
       confirmed: Confirmado
@@ -115,84 +119,84 @@ pt:
       topic: Tópico
     reports:
       comment:
-        label: Commentário
-        none: None
-      delete: Deletar
+        label: Comentário
+        none: Nenhum
+      delete: Eliminar
       id: ID
-      mark_as_resolved: Marque como resolvido
-      report: 'Report #%{id}'
-      reported_account: Conta reportada
-      reported_by: Reportado por
+      mark_as_resolved: Marcar como resolvido
+      report: 'Denúncia #%{id}'
+      reported_account: Conta denunciada
+      reported_by: Denúnciada por
       resolved: Resolvido
       silence_account: Conta silenciada
-      status: Status
+      status: Estado
       suspend_account: Conta suspensa
       target: Target
-      title: Reports
-      unresolved: Unresolved
-      view: View
+      title: Denúncias
+      unresolved: Por resolver
+      view: Ver
     settings:
       click_to_edit: Clique para editar
       contact_information:
-        email: Entre um endereço de email público
-        label: Informação de contato
-        username: Entre com usuário
+        email: Inserir um endereço de email para tornar público
+        label: Informação de contacto
+        username: Insira um nome de utilizador
       registrations:
         closed_message:
-          desc_html: Mostrar na página inicial quando registros estão fecados<br/>Você pode usar tags HTML
-          title: Mensagem de registro fechados
+          desc_html: Mostrar na página inicial quando registos estão encerrados<br/>Podes usar tags HTML
+          title: Mensagem de registos encerrados
         open:
           disabled: Desabilitado
           enabled: Habilitado
-          title: Aberto para registro
+          title: Aceitar novos registos
       setting: Preferências
       site_description:
-        desc_html: Mostrar como parágrafo e usado como meta tag.<br/>Vôce pode usar tags HTML, em particular <code>&lt;a&gt;</code> e <code>&lt;em&gt;</code>.
+        desc_html: Mostrar como parágrafo na página inicial e usado como meta tag.<br/>Podes usar tags HTML, em particular <code>&lt;a&gt;</code> e <code>&lt;em&gt;</code>.
         title: Descrição do site
       site_description_extended:
-        desc_html: Mostrar na página de informação extendiada <br/>Você pode usar tags HTML
-        title: Descrição extendida do site
+        desc_html: Mostrar na página de mais informações<br/>Podes usar tags HTML
+        title: Página de mais informações
       site_title: Título do site
       title: Preferências do site
     title: Administração
   application_mailer:
-    settings: 'Mudar preferências de email: %{link}'
-    signature: notificações Mastodon de %{instance}
-    view: 'View:'
+    settings: 'Alterar preferências de email: %{link}'
+    signature: notificações Mastodon do %{instance}
+    view: 'Ver:'
   applications:
-    invalid_url: URL dada é inválida
+    invalid_url: O URL é inválido
   auth:
-    change_password: Mudar senha
-    didnt_get_confirmation: Não recebeu instruções de confirmação?
-    forgot_password: Esqueceu a senha?
+    change_password: Alterar palavra-passe
+    didnt_get_confirmation: Não recebeu o email de confirmação?
+    forgot_password: Esqueceste a palavra-passe?
     login: Entrar
     register: Registar
     resend_confirmation: Reenviar instruções de confirmação
-    reset_password: Resetar senha
-    set_new_password: Editar password
+    reset_password: Criar nova palavra-passe
+    set_new_password: Editar palavra-passe
   generic:
-    changes_saved_msg: Mudanças guardadas!
+    changes_saved_msg: Alteraçes guardadas!
     powered_by: powered by %{link}
     save_changes: Guardar alterações
     validation_errors:
-      one: Algo não está correto. Por favor reveja o erro abaixo
-      other: Algo não está correto. Por favor reveja os %{count} erros abaixo
+      one: Algo não está correcto. Por favor vê o erro abaixo
+      other: Algo não está correto. Por favor vê os %{count} erros abaixo
   notification_mailer:
     favourite:
-      body: 'O seu post foi favoritado por %{name}:'
-      subject: "%{name} favouritou o seu post"
+      body: 'O teu post foi adicionado aos favoritos por %{name}:'
+      subject: "%{name} adicionou o teu post aos favoritos"
     follow:
-      body: "%{name} seguiu você!"
-      subject: "%{name} segue você"
+      body: "%{name} é teu seguidor!"
+      subject: "%{name} começou a seguir-te"
     mention:
-      body: 'Você foi mencionado por %{name} em:'
-      subject: Foi mencionado por %{name}
+      body: 'Foste mencionado por %{name}:'
+      subject: "%{name} mencionou-te"
     reblog:
-      body: 'O seu post foi reblogado por %{name}:'
-      subject: "%{name} reblogou o seu post"
+      body: 'O teu post foi partilhado por %{name}:'
+      subject: "%{name} partilhou o teu post"
   pagination:
-    next: Next
-    prev: Prev
+    next: Seguinte
+    prev: Anterior
   settings:
     edit_profile: Editar perfil
     preferences: Preferências
diff --git a/config/locales/simple_form.nl.yml b/config/locales/simple_form.nl.yml
index 5bc38a87b..c0539fd63 100644
--- a/config/locales/simple_form.nl.yml
+++ b/config/locales/simple_form.nl.yml
@@ -6,39 +6,39 @@ nl:
         avatar: PNG, GIF of JPG. Maximaal 2MB. Wordt teruggeschaald naar 120x120px
         display_name: Maximaal 30 tekens
         header: PNG, GIF of JPG. Maximaal 2MB. Wordt teruggeschaald naar 700x335px
-        locked: Vereist dat je handmatig volgers accepteert en stelt standaard plaatsen berichten privacy in op alleen-volgers
-        note: Maximaal 160 characters
+        locked: Vereist dat je handmatig volgers moet accepteren en stelt de privacy van toots standaard in op alleen volgers
+        note: Maximaal 160 tekens
       imports:
-        data: CSV file geëxporteerd van een andere Mastodon server
+        data: CSV-bestand dat op een andere Mastodon-server werd geëxporteerd 
     labels:
       defaults:
         avatar: Avatar
-        confirm_new_password: Bevestig nieuw wachtwoord
-        confirm_password: Bevestig wachtwoord
-        current_password: Huidige wachtwoord
+        confirm_new_password: Nieuw wachtwoord bevestigen
+        confirm_password: Wachtwoord bevestigen
+        current_password: Huidig wachtwoord
         data: Gegevens
         display_name: Weergavenaam
         email: E-mailadres
-        header: Kop
+        header: Omslagfoto
         locale: Taal
         locked: Maak account besloten
         new_password: Nieuwe wachtwoord
         note: Bio
-        otp_attempt: Twee-factor code
+        otp_attempt: Tweestaps-aanmeldcode
         password: Wachtwoord
-        setting_default_privacy: Berichten privacy
-        type: Import type
+        setting_default_privacy: Tootprivacy
+        type: Importtype
         username: gebruikersnaam
       interactions:
-        must_be_follower: Blokkeermeldingen van niet-volgers
-        must_be_following: Blokkeer meldingen van mensen die je niet volgt
+        must_be_follower: Blokkeermeldingen van mensen die jou niet volgen
+        must_be_following: Blokkeermeldingen van mensen die jij niet volgt
       notification_emails:
-        digest: Verstuur samenvattingse-mails
-        favourite: Verstuur een e-mail wanneer iemand je status als favoriet markeert
-        follow: Verstuur een e-mail wanneer iemand je volgt
-        follow_request: Verstuur een e-mail wanneer iemand je wil volgen
-        mention: Verstuur een e-mail wanneer iemand je vermeld
-        reblog: Verstuur een e-mail wanneer iemand je status boost
+        digest: Verstuur periodiek e-mails met een samenvatting
+        favourite: Verstuur een e-mail wanneer iemand jouw toot als favoriet markeert
+        follow: Verstuur een e-mail wanneer iemand jou volgt
+        follow_request: Verstuur een e-mail wanneer iemand jou wilt volgen
+        mention: Verstuur een e-mail wanneer iemand jou vermeld
+        reblog: Verstuur een e-mail wanneer iemand jouw toot heeft geboost
     'no': 'Nee'
     required:
       mark: "*"
diff --git a/config/locales/simple_form.pt-BR.yml b/config/locales/simple_form.pt-BR.yml
new file mode 100644
index 000000000..28f7eeea8
--- /dev/null
+++ b/config/locales/simple_form.pt-BR.yml
@@ -0,0 +1,30 @@
+---
+pt-BR:
+  simple_form:
+    labels:
+      defaults:
+        avatar: Avatar
+        confirm_new_password: Confirme nova senha
+        confirm_password: Confirme a senha
+        current_password: Senha atual
+        display_name: Nome
+        email: Endereço de email
+        header: Header
+        locale: Linguagem
+        new_password: Nova senha
+        note: Biografia
+        password: Senha
+        username: Usuário
+      interactions:
+        must_be_follower: Bloquear notificações de não-seguidores
+        must_be_following: Bloquear notificações de pessoas que você
+      notification_emails:
+        favourite: Enviar email quando alguém favorita um post seu
+        follow: Enviar email quando alguém seguir você
+        mention: Enviar email quando alguém mencionar você
+        reblog: Enviar email quando alguém reblogar um post seu
+    'no': 'Não'
+    required:
+      mark: "*"
+      text: necessário
+    'yes': 'Sim'
diff --git a/config/locales/simple_form.pt.yml b/config/locales/simple_form.pt.yml
index e8b5e2d7f..ba3326b23 100644
--- a/config/locales/simple_form.pt.yml
+++ b/config/locales/simple_form.pt.yml
@@ -1,28 +1,47 @@
 ---
 pt:
   simple_form:
+    hints:
+      defaults:
+        avatar: PNG, GIF ou JPG. No máximo 2MB. Vai ser reduzido para 120x120px
+        display_name: No máximo 30 caracteres
+        header: PNG, GIF or JPG. No máximo 2MB. Vai ser reduzido para 700x335px
+        locked: Requer que manualmente aproves seguidores e torna o default dos teus posts para privados (apenas seguidores)
+        note: No máximo 160 caracteres
+      imports:
+        data: Ficheiro CSV exportado de outra instância do Mastodon
+      sessions:
+        otp: Insere o código o código de autenticação de dois fatores do teu telefone ou utiliza um código de recuperação de acesso.
     labels:
       defaults:
-        avatar: Avatar
-        confirm_new_password: Confirme nova senha
-        confirm_password: Confirme a senha
-        current_password: Senha atual
+        avatar: Imagem de Perfil
+        confirm_new_password: Confirme nova palavra-passe
+        confirm_password: Confirme a palavra-passe
+        current_password: Palavra-passe actual
+        data: Data
         display_name: Nome
         email: Endereço de email
-        header: Header
-        locale: Linguagem
-        new_password: Nova senha
+        header: Cabeçalho
+        locale: Língua
+        locked: Tornar conta privada
+        new_password: Nova palavra-passe
         note: Biografia
-        password: Senha
-        username: Usuário
+        otp_attempt: Código de autenticação de dois fatores
+        password: Palavra-passe
+        setting_boost_modal: Pedir confirmação antes de partilhar um post
+        setting_default_privacy: Privacidade padrão de posts
+        severity: Severity
+        type: Import type
+        username: Utilizador
       interactions:
         must_be_follower: Bloquear notificações de não-seguidores
-        must_be_following: Bloquear notificações de pessoas que você
+        must_be_following: Bloquear notificações de pessoas que não segues
       notification_emails:
-        favourite: Enviar email quando alguém favorita um post seu
-        follow: Enviar email quando alguém seguir você
-        mention: Enviar email quando alguém mencionar você
-        reblog: Enviar email quando alguém reblogar um post seu
+        digest: Enviar um email da actividade nesta instância
+        favourite: Enviar email quando alguém adiciona um post teu aos favoritos
+        follow: Enviar email quando alguém te segue
+        mention: Enviar email quando alguém te menciona
+        reblog: Enviar email quando alguém partilhar um post teu
     'no': 'Não'
     required:
       mark: "*"
diff --git a/config/navigation.rb b/config/navigation.rb
index 6e2bcd001..7470fea8c 100644
--- a/config/navigation.rb
+++ b/config/navigation.rb
@@ -19,8 +19,8 @@ SimpleNavigation::Configuration.run do |navigation|
       admin.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_url, highlights_on: %r{/admin/accounts}
       admin.item :pubsubhubbubs, safe_join([fa_icon('paper-plane-o fw'), t('admin.pubsubhubbub.title')]), admin_pubsubhubbub_index_url
       admin.item :domain_blocks, safe_join([fa_icon('lock fw'), t('admin.domain_blocks.title')]), admin_domain_blocks_url, highlights_on: %r{/admin/domain_blocks}
-      admin.item :sidekiq, safe_join([fa_icon('diamond fw'), 'Sidekiq']), sidekiq_url
-      admin.item :pghero, safe_join([fa_icon('database fw'), 'PgHero']), pghero_url
+      admin.item :sidekiq, safe_join([fa_icon('diamond fw'), 'Sidekiq']), sidekiq_url, link_html: { target: 'sidekiq' }
+      admin.item :pghero, safe_join([fa_icon('database fw'), 'PgHero']), pghero_url, link_html: { target: 'pghero' }
       admin.item :settings, safe_join([fa_icon('cogs fw'), t('admin.settings.title')]), admin_settings_url
     end
 
diff --git a/config/routes.rb b/config/routes.rb
index fd186c320..4bb3393b8 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -16,7 +16,7 @@ Rails.application.routes.draw do
   end
 
   get '.well-known/host-meta', to: 'well_known/host_meta#show', as: :host_meta, defaults: { format: 'xml' }
-  get '.well-known/webfinger', to: 'well_known/webfinger#show', as: :webfinger, defaults: { format: 'json' }
+  get '.well-known/webfinger', to: 'well_known/webfinger#show', as: :webfinger
 
   devise_for :users, path: 'auth', controllers: {
     sessions:           'auth/sessions',
diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake
index a1947ea0e..7dd7b5cd1 100644
--- a/lib/tasks/mastodon.rake
+++ b/lib/tasks/mastodon.rake
@@ -103,6 +103,12 @@ namespace :mastodon do
         User.where(id: batch.map(&:id)).delete_all
       end
     end
+
+    desc 'List all admin users'
+    task admins: :environment do
+      puts 'Admin user emails:'
+      puts User.admins.map(&:email).join("\n")
+    end
   end
 
   namespace :settings do
@@ -145,8 +151,8 @@ namespace :mastodon do
 
       Account.unscoped.where(avatar_content_type: 'image/gif').or(Account.unscoped.where(header_content_type: 'image/gif')).find_each do |account|
         begin
-          account.avatar.reprocess!
-          account.header.reprocess!
+          account.avatar.reprocess! if account.avatar_content_type == 'image/gif' && !account.avatar.exists?(:static)
+          account.header.reprocess! if account.header_content_type == 'image/gif' && !account.header.exists?(:static)
         rescue StandardError => e
           Rails.logger.error "Error while generating static avatars/headers for account #{account.id}: #{e}"
           next
diff --git a/public/mask-icon.svg b/public/mask-icon.svg
new file mode 100644
index 000000000..c35230117
--- /dev/null
+++ b/public/mask-icon.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" height="16" width="16"><path d="M8 .1A7.9 7.9 0 0 0 .1 8 7.9 7.9 0 0 0 8 15.9 7.9 7.9 0 0 0 15.9 8 7.9 7.9 0 0 0 8 .1zm-.04 4.28h1.7c-.33.23-.44.9-.44 1.25v3.2c0 .7-.55 1.24-1.26 1.24-.7 0-1.26-.55-1.26-1.25v-3.2c0-.68.55-1.24 1.26-1.24zm-4.36.78c.7 0 1.26.55 1.26 1.24v3.2c0 .34.1 1.02.43 1.24H3.6c-.7 0-1.26-.55-1.26-1.24V6.4c0-.7.55-1.24 1.26-1.24zm8.7 0c.7 0 1.26.55 1.26 1.24v3.2c0 .7-.56 1.24-1.27 1.24h-1.7c.32-.22.43-.9.43-1.24V6.4c0-.7.56-1.24 1.26-1.24z" fill="#000"/></svg>
\ No newline at end of file
diff --git a/spec/controllers/auth/registrations_controller_spec.rb b/spec/controllers/auth/registrations_controller_spec.rb
index 6b26e6693..c2141766e 100644
--- a/spec/controllers/auth/registrations_controller_spec.rb
+++ b/spec/controllers/auth/registrations_controller_spec.rb
@@ -16,9 +16,12 @@ RSpec.describe Auth::RegistrationsController, type: :controller do
   end
 
   describe 'POST #create' do
+    let(:accept_language) { Rails.application.config.i18n.available_locales.sample.to_s }
+
     before do
       Setting.open_registrations = true
       request.env["devise.mapping"] = Devise.mappings[:user]
+      request.headers["Accept-Language"] = accept_language
       post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678' } }
     end
 
@@ -27,7 +30,9 @@ RSpec.describe Auth::RegistrationsController, type: :controller do
     end
 
     it 'creates user' do
-      expect(User.find_by(email: 'test@example.com')).to_not be_nil
+      user = User.find_by(email: 'test@example.com')
+      expect(user).to_not be_nil
+      expect(user.locale).to eq(accept_language)
     end
   end
 end
diff --git a/spec/controllers/media_controller_spec.rb b/spec/controllers/media_controller_spec.rb
new file mode 100644
index 000000000..ebf6aa006
--- /dev/null
+++ b/spec/controllers/media_controller_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe MediaController do
+  describe '#show' do
+    it 'redirects to the file url when attached to a status' do
+      status = Fabricate(:status)
+      media_attachment = Fabricate(:media_attachment, status: status)
+      get :show, params: { id: media_attachment.to_param }
+
+      expect(response).to redirect_to(media_attachment.file.url(:original))
+    end
+
+    it 'responds with missing when there is not an attached status' do
+      media_attachment = Fabricate(:media_attachment, status: nil)
+      get :show, params: { id: media_attachment.to_param }
+
+      expect(response).to have_http_status(:missing)
+    end
+
+    it 'raises when shortcode cant be found' do
+      get :show, params: { id: 'missing' }
+
+      expect(response).to have_http_status(:missing)
+    end
+
+    it 'raises when not permitted to view' do
+      status = Fabricate(:status)
+      media_attachment = Fabricate(:media_attachment, status: status)
+      allow_any_instance_of(Status).to receive(:permitted?).and_return(false)
+      get :show, params: { id: media_attachment.to_param }
+
+      expect(response).to have_http_status(:missing)
+    end
+  end
+end
diff --git a/spec/helpers/instance_helper_spec.rb b/spec/helpers/instance_helper_spec.rb
new file mode 100644
index 000000000..c42ed6938
--- /dev/null
+++ b/spec/helpers/instance_helper_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe InstanceHelper do
+  describe 'site_title' do
+    it 'Uses the Setting.site_title value when it exists' do
+      Setting.site_title = 'New site title'
+
+      expect(helper.site_title).to eq 'New site title'
+    end
+
+    it 'returns empty string when Setting.site_title is nil' do
+      Setting.site_title = nil
+
+      expect(helper.site_title).to eq ''
+    end
+  end
+
+  describe 'site_hostname' do
+    around(:each) do |example|
+      before = Rails.configuration.x.local_domain
+      example.run
+      Rails.configuration.x.local_domain = before
+    end
+
+    it 'returns the local domain value' do
+      Rails.configuration.x.local_domain = 'example.com'
+
+      expect(helper.site_hostname).to eq 'example.com'
+    end
+  end
+end
diff --git a/spec/helpers/site_title_helper_spec.rb b/spec/helpers/site_title_helper_spec.rb
deleted file mode 100644
index 8cfd9cba1..000000000
--- a/spec/helpers/site_title_helper_spec.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-require "rails_helper"
-
-describe "site_title" do
-  it "Uses the Setting.site_title value when it exists" do
-    Setting.site_title = "New site title"
-
-    expect(helper.site_title).to eq "New site title"
-  end
-
-  it "returns empty string when Setting.site_title is nil" do
-    Setting.site_title = nil
-
-    expect(helper.site_title).to eq ""
-  end
-end
diff --git a/spec/i18n_spec.rb b/spec/i18n_spec.rb
deleted file mode 100644
index 138d25569..000000000
--- a/spec/i18n_spec.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-require 'i18n/tasks'
-
-RSpec.describe 'I18n' do
-  let(:i18n)         { I18n::Tasks::BaseTask.new }
-  let(:missing_keys) { i18n.missing_keys }
-  let(:unused_keys)  { i18n.unused_keys }
-
-  xit 'does not have missing keys' do
-    expect(missing_keys).to be_empty, "Missing #{missing_keys.leaves.count} i18n keys, run `i18n-tasks missing' to show them"
-  end
-
-  xit 'does not have unused keys' do
-    expect(unused_keys).to be_empty, "#{unused_keys.leaves.count} unused i18n keys, run `i18n-tasks unused' to show them"
-  end
-end
diff --git a/spec/lib/atom_serializer_spec.rb b/spec/lib/atom_serializer_spec.rb
new file mode 100644
index 000000000..0009e41a9
--- /dev/null
+++ b/spec/lib/atom_serializer_spec.rb
@@ -0,0 +1,21 @@
+require 'rails_helper'
+
+RSpec.describe AtomSerializer do
+  describe '#author' do
+    it 'returns dumpable XML with emojis' do
+      account = Fabricate(:account, display_name: '💩')
+      xml     = AtomSerializer.render(AtomSerializer.new.author(account))
+
+      expect(xml).to be_a String
+      expect(xml).to match(/<poco:displayName>💩<\/poco:displayName>/)
+    end
+
+    it 'returns dumpable XML with invalid characters like \b and \v' do
+      account = Fabricate(:account, display_name: "im l33t\b haxo\b\vr")
+      xml     = AtomSerializer.render(AtomSerializer.new.author(account))
+
+      expect(xml).to be_a String
+      expect(xml).to match(/<poco:displayName>im l33t haxor<\/poco:displayName>/)
+    end
+  end
+end
diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb
index 46c1ff63c..c39e7986e 100644
--- a/spec/models/account_spec.rb
+++ b/spec/models/account_spec.rb
@@ -245,6 +245,23 @@ RSpec.describe Account, type: :model do
     end
   end
 
+  describe '.triadic_closures' do
+    it 'finds accounts you dont follow which are followed by accounts you do follow' do
+      me = Fabricate(:account)
+      friend = Fabricate(:account)
+      friends_friend = Fabricate(:account)
+      me.follow!(friend)
+      friend.follow!(friends_friend)
+
+      both_follow = Fabricate(:account)
+      me.follow!(both_follow)
+      friend.follow!(both_follow)
+
+      results = Account.triadic_closures(me)
+      expect(results).to eq [friends_friend]
+    end
+  end
+
   describe '.find_local' do
     before do
       Fabricate(:account, username: 'Alice')
diff --git a/spec/requests/webfinger_request_spec.rb b/spec/requests/webfinger_request_spec.rb
index b5690d22f..a17d6cc22 100644
--- a/spec/requests/webfinger_request_spec.rb
+++ b/spec/requests/webfinger_request_spec.rb
@@ -1,33 +1,48 @@
-require "rails_helper"
+require 'rails_helper'
 
-describe "The webfinger route" do
+describe 'The webfinger route' do
   let(:alice) { Fabricate(:account, username: 'alice') }
 
-  describe "requested without accepts headers" do
-    it "returns a json response" do
-      get webfinger_url, params: { resource: alice.to_webfinger_s }
+  describe 'requested with standard accepts headers' do
+    it 'returns a json response' do
+      get webfinger_url(resource: alice.to_webfinger_s)
 
       expect(response).to have_http_status(:success)
-      expect(response.content_type).to eq "application/jrd+json"
+      expect(response.content_type).to eq 'application/jrd+json'
     end
   end
 
-  describe "requested with html in accepts headers" do
-    it "returns a json response" do
-      headers = { 'HTTP_ACCEPT' => 'text/html' }
-      get webfinger_url, params: { resource: alice.to_webfinger_s }, headers: headers
+  describe 'asking for xml format' do
+    it 'returns an xml response for xml format' do
+      get webfinger_url(resource: alice.to_webfinger_s, format: :xml)
+
+      expect(response).to have_http_status(:success)
+      expect(response.content_type).to eq 'application/xrd+xml'
+    end
+
+    it 'returns an xml response for xml accept header' do
+      headers = { 'HTTP_ACCEPT' => 'application/xrd+xml' }
+      get webfinger_url(resource: alice.to_webfinger_s), headers: headers
 
       expect(response).to have_http_status(:success)
-      expect(response.content_type).to eq "application/jrd+json"
+      expect(response.content_type).to eq 'application/xrd+xml'
     end
   end
 
-  describe "requested with xml format" do
-    it "returns an xml response" do
-      get webfinger_url(resource: alice.to_webfinger_s, format: :xml)
+  describe 'asking for json format' do
+    it 'returns a json response for json format' do
+      get webfinger_url(resource: alice.to_webfinger_s, format: :json)
+
+      expect(response).to have_http_status(:success)
+      expect(response.content_type).to eq 'application/jrd+json'
+    end
+
+    it 'returns a json response for json accept header' do
+      headers = { 'HTTP_ACCEPT' => 'application/jrd+json' }
+      get webfinger_url(resource: alice.to_webfinger_s), headers: headers
 
       expect(response).to have_http_status(:success)
-      expect(response.content_type).to eq "application/xrd+xml"
+      expect(response.content_type).to eq 'application/jrd+json'
     end
   end
 end
diff --git a/spec/routing/well_known_routes_spec.rb b/spec/routing/well_known_routes_spec.rb
index 9540c3de3..2e25605c2 100644
--- a/spec/routing/well_known_routes_spec.rb
+++ b/spec/routing/well_known_routes_spec.rb
@@ -10,6 +10,6 @@ end
 describe 'the webfinger route' do
   it 'routes to correct place with json format' do
     expect(get('/.well-known/webfinger')).
-      to route_to('well_known/webfinger#show', format: 'json')
+      to route_to('well_known/webfinger#show')
   end
 end
diff --git a/spec/services/account_search_service_spec.rb b/spec/services/account_search_service_spec.rb
index fa421c443..723623833 100644
--- a/spec/services/account_search_service_spec.rb
+++ b/spec/services/account_search_service_spec.rb
@@ -25,6 +25,18 @@ describe AccountSearchService do
     end
 
     describe 'searching local and remote users' do
+      describe "when only '@'" do
+        before do
+          allow(Account).to receive(:find_remote)
+          allow(Account).to receive(:search_for)
+          subject.call('@', 10)
+        end
+
+        it 'uses find_remote with empty query to look for local accounts' do
+          expect(Account).to have_received(:find_remote).with('', nil)
+        end
+      end
+
       describe 'when no domain' do
         before do
           allow(Account).to receive(:find_remote)
diff --git a/storybook/config.js b/storybook/config.js
index 4a111a8b9..924eadf49 100644
--- a/storybook/config.js
+++ b/storybook/config.js
@@ -17,6 +17,7 @@ window.React     = React;
 function loadStories () {
   require('./stories/loading_indicator.story.jsx');
   require('./stories/button.story.jsx');
+  require('./stories/character_counter.story.jsx');
   require('./stories/autosuggest_textarea.story.jsx');
 }
 
diff --git a/storybook/stories/character_counter.story.jsx b/storybook/stories/character_counter.story.jsx
new file mode 100644
index 000000000..931d8a037
--- /dev/null
+++ b/storybook/stories/character_counter.story.jsx
@@ -0,0 +1,20 @@
+import { storiesOf } from '@kadira/storybook';
+import CharacterCounter from '../../app/assets/javascripts/components/features/compose/components/character_counter';
+
+storiesOf('CharacterCounter', module)
+  .add('no text', () => {
+    const text = '';
+    return <CharacterCounter text={text} max="500" />;
+  })
+  .add('a few strings text', () => {
+    const text = '0123456789';
+    return <CharacterCounter text={text} max="500" />;
+  })
+  .add('the same text', () => {
+    const text = '01234567890123456789';
+    return <CharacterCounter text={text} max="20" />;
+  })
+  .add('over text', () => {
+    const text = '01234567890123456789012345678901234567890123456789';
+    return <CharacterCounter text={text} max="10" />;
+  });
diff --git a/streaming/index.js b/streaming/index.js
index a1e7eaca7..366a39a6a 100644
--- a/streaming/index.js
+++ b/streaming/index.js
@@ -1,3 +1,5 @@
+import os from 'os';
+import cluster from 'cluster';
 import dotenv from 'dotenv'
 import express from 'express'
 import http from 'http'
@@ -14,300 +16,318 @@ dotenv.config({
   path: env === 'production' ? '.env.production' : '.env'
 })
 
-const pgConfigs = {
-  development: {
-    database: 'mastodon_development',
-    host:     '/var/run/postgresql',
-    max:      10
-  },
-
-  production: {
-    user:     process.env.DB_USER || 'mastodon',
-    password: process.env.DB_PASS || '',
-    database: process.env.DB_NAME || 'mastodon_production',
-    host:     process.env.DB_HOST || 'localhost',
-    port:     process.env.DB_PORT || 5432,
-    max:      10
+if (cluster.isMaster) {
+  // cluster master
+
+  const core = +process.env.STREAMING_CLUSTER_NUM || (env === 'development' ? 1 : os.cpus().length - 1)
+  const fork = () => {
+    const worker = cluster.fork();
+    worker.on('exit', (code, signal) => {
+      log.error(`Worker died with exit code ${code}, signal ${signal} received.`);
+      setTimeout(() => fork(), 0);
+    });
+  };
+  for (let i = 0; i < core; i++) fork();
+  log.info(`Starting streaming API server master with ${core} workers`)
+
+} else {
+  // cluster worker
+
+  const pgConfigs = {
+    development: {
+      database: 'mastodon_development',
+      host:     '/var/run/postgresql',
+      max:      10
+    },
+
+    production: {
+      user:     process.env.DB_USER || 'mastodon',
+      password: process.env.DB_PASS || '',
+      database: process.env.DB_NAME || 'mastodon_production',
+      host:     process.env.DB_HOST || 'localhost',
+      port:     process.env.DB_PORT || 5432,
+      max:      10
+    }
   }
-}
 
-const app    = express()
-const pgPool = new pg.Pool(pgConfigs[env])
-const server = http.createServer(app)
-const wss    = new WebSocket.Server({ server })
-
-const redisClient = redis.createClient({
-  host:     process.env.REDIS_HOST     || '127.0.0.1',
-  port:     process.env.REDIS_PORT     || 6379,
-  password: process.env.REDIS_PASSWORD
-})
+  const app    = express()
+  const pgPool = new pg.Pool(pgConfigs[env])
+  const server = http.createServer(app)
+  const wss    = new WebSocket.Server({ server })
 
-const subs = {}
-
-redisClient.on('pmessage', (_, channel, message) => {
-  const callbacks = subs[channel]
+  const redisClient = redis.createClient({
+    host:     process.env.REDIS_HOST     || '127.0.0.1',
+    port:     process.env.REDIS_PORT     || 6379,
+    password: process.env.REDIS_PASSWORD
+  })
 
-  log.silly(`New message on channel ${channel}`)
+  const subs = {}
 
-  if (!callbacks) {
-    return
-  }
+  redisClient.on('pmessage', (_, channel, message) => {
+    const callbacks = subs[channel]
 
-  callbacks.forEach(callback => callback(message))
-})
+    log.silly(`New message on channel ${channel}`)
 
-redisClient.psubscribe('timeline:*')
+    if (!callbacks) {
+      return
+    }
 
-const subscribe = (channel, callback) => {
-  log.silly(`Adding listener for ${channel}`)
-  subs[channel] = subs[channel] || []
-  subs[channel].push(callback)
-}
+    callbacks.forEach(callback => callback(message))
+  })
 
-const unsubscribe = (channel, callback) => {
-  log.silly(`Removing listener for ${channel}`)
-  subs[channel] = subs[channel].filter(item => item !== callback)
-}
+  redisClient.psubscribe('timeline:*')
 
-const allowCrossDomain = (req, res, next) => {
-  res.header('Access-Control-Allow-Origin', '*')
-  res.header('Access-Control-Allow-Headers', 'Authorization, Accept, Cache-Control')
-  res.header('Access-Control-Allow-Methods', 'GET, OPTIONS')
+  const subscribe = (channel, callback) => {
+    log.silly(`Adding listener for ${channel}`)
+    subs[channel] = subs[channel] || []
+    subs[channel].push(callback)
+  }
 
-  next()
-}
+  const unsubscribe = (channel, callback) => {
+    log.silly(`Removing listener for ${channel}`)
+    subs[channel] = subs[channel].filter(item => item !== callback)
+  }
 
-const setRequestId = (req, res, next) => {
-  req.requestId = uuid.v4()
-  res.header('X-Request-Id', req.requestId)
+  const allowCrossDomain = (req, res, next) => {
+    res.header('Access-Control-Allow-Origin', '*')
+    res.header('Access-Control-Allow-Headers', 'Authorization, Accept, Cache-Control')
+    res.header('Access-Control-Allow-Methods', 'GET, OPTIONS')
 
-  next()
-}
+    next()
+  }
 
-const accountFromToken = (token, req, next) => {
-  pgPool.connect((err, client, done) => {
-    if (err) {
-      next(err)
-      return
-    }
+  const setRequestId = (req, res, next) => {
+    req.requestId = uuid.v4()
+    res.header('X-Request-Id', req.requestId)
 
-    client.query('SELECT oauth_access_tokens.resource_owner_id, users.account_id FROM oauth_access_tokens INNER JOIN users ON oauth_access_tokens.resource_owner_id = users.id WHERE oauth_access_tokens.token = $1 LIMIT 1', [token], (err, result) => {
-      done()
+    next()
+  }
 
+  const accountFromToken = (token, req, next) => {
+    pgPool.connect((err, client, done) => {
       if (err) {
         next(err)
         return
       }
 
-      if (result.rows.length === 0) {
-        err = new Error('Invalid access token')
-        err.statusCode = 401
-
-        next(err)
-        return
-      }
+      client.query('SELECT oauth_access_tokens.resource_owner_id, users.account_id FROM oauth_access_tokens INNER JOIN users ON oauth_access_tokens.resource_owner_id = users.id WHERE oauth_access_tokens.token = $1 LIMIT 1', [token], (err, result) => {
+        done()
 
-      req.accountId = result.rows[0].account_id
-
-      next()
-    })
-  })
-}
+        if (err) {
+          next(err)
+          return
+        }
 
-const authenticationMiddleware = (req, res, next) => {
-  if (req.method === 'OPTIONS') {
-    next()
-    return
-  }
+        if (result.rows.length === 0) {
+          err = new Error('Invalid access token')
+          err.statusCode = 401
 
-  const authorization = req.get('Authorization')
+          next(err)
+          return
+        }
 
-  if (!authorization) {
-    const err = new Error('Missing access token')
-    err.statusCode = 401
+        req.accountId = result.rows[0].account_id
 
-    next(err)
-    return
+        next()
+      })
+    })
   }
 
-  const token = authorization.replace(/^Bearer /, '')
+  const authenticationMiddleware = (req, res, next) => {
+    if (req.method === 'OPTIONS') {
+      next()
+      return
+    }
 
-  accountFromToken(token, req, next)
-}
+    const authorization = req.get('Authorization')
 
-const errorMiddleware = (err, req, res, next) => {
-  log.error(req.requestId, err)
-  res.writeHead(err.statusCode || 500, { 'Content-Type': 'application/json' })
-  res.end(JSON.stringify({ error: err.statusCode ? `${err}` : 'An unexpected error occurred' }))
-}
+    if (!authorization) {
+      const err = new Error('Missing access token')
+      err.statusCode = 401
 
-const placeholders = (arr, shift = 0) => arr.map((_, i) => `$${i + 1 + shift}`).join(', ');
+      next(err)
+      return
+    }
 
-const streamFrom = (id, req, output, attachCloseHandler, needsFiltering = false) => {
-  log.verbose(req.requestId, `Starting stream from ${id} for ${req.accountId}`)
+    const token = authorization.replace(/^Bearer /, '')
 
-  const listener = message => {
-    const { event, payload, queued_at } = JSON.parse(message)
+    accountFromToken(token, req, next)
+  }
 
-    const transmit = () => {
-      const now   = new Date().getTime()
-      const delta = now - queued_at;
+  const errorMiddleware = (err, req, res, next) => {
+    log.error(req.requestId, err)
+    res.writeHead(err.statusCode || 500, { 'Content-Type': 'application/json' })
+    res.end(JSON.stringify({ error: err.statusCode ? `${err}` : 'An unexpected error occurred' }))
+  }
 
-      log.silly(req.requestId, `Transmitting for ${req.accountId}: ${event} ${payload} Delay: ${delta}ms`)
-      output(event, payload)
-    }
+  const placeholders = (arr, shift = 0) => arr.map((_, i) => `$${i + 1 + shift}`).join(', ');
 
-    // Only messages that may require filtering are statuses, since notifications
-    // are already personalized and deletes do not matter
-    if (needsFiltering && event === 'update') {
-      pgPool.connect((err, client, done) => {
-        if (err) {
-          log.error(err)
-          return
-        }
+  const streamFrom = (id, req, output, attachCloseHandler, needsFiltering = false) => {
+    log.verbose(req.requestId, `Starting stream from ${id} for ${req.accountId}`)
+
+    const listener = message => {
+      const { event, payload, queued_at } = JSON.parse(message)
 
-        const unpackedPayload  = JSON.parse(payload)
-        const targetAccountIds = [unpackedPayload.account.id].concat(unpackedPayload.mentions.map(item => item.id)).concat(unpackedPayload.reblog ? [unpackedPayload.reblog.account.id] : [])
+      const transmit = () => {
+        const now   = new Date().getTime()
+        const delta = now - queued_at;
 
-        client.query(`SELECT target_account_id FROM blocks WHERE account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 1)}) UNION SELECT target_account_id FROM mutes WHERE account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 1)})`, [req.accountId].concat(targetAccountIds), (err, result) => {
-          done()
+        log.silly(req.requestId, `Transmitting for ${req.accountId}: ${event} ${payload} Delay: ${delta}ms`)
+        output(event, payload)
+      }
 
+      // Only messages that may require filtering are statuses, since notifications
+      // are already personalized and deletes do not matter
+      if (needsFiltering && event === 'update') {
+        pgPool.connect((err, client, done) => {
           if (err) {
             log.error(err)
             return
           }
 
-          if (result.rows.length > 0) {
-            return
-          }
+          const unpackedPayload  = JSON.parse(payload)
+          const targetAccountIds = [unpackedPayload.account.id].concat(unpackedPayload.mentions.map(item => item.id)).concat(unpackedPayload.reblog ? [unpackedPayload.reblog.account.id] : [])
+
+          client.query(`SELECT target_account_id FROM blocks WHERE account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 1)}) UNION SELECT target_account_id FROM mutes WHERE account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 1)})`, [req.accountId].concat(targetAccountIds), (err, result) => {
+            done()
+
+            if (err) {
+              log.error(err)
+              return
+            }
 
-          transmit()
+            if (result.rows.length > 0) {
+              return
+            }
+
+            transmit()
+          })
         })
-      })
-    } else {
-      transmit()
+      } else {
+        transmit()
+      }
     }
+
+    subscribe(id, listener)
+    attachCloseHandler(id, listener)
   }
 
-  subscribe(id, listener)
-  attachCloseHandler(id, listener)
-}
+  // Setup stream output to HTTP
+  const streamToHttp = (req, res) => {
+    res.setHeader('Content-Type', 'text/event-stream')
+    res.setHeader('Transfer-Encoding', 'chunked')
 
-// Setup stream output to HTTP
-const streamToHttp = (req, res) => {
-  res.setHeader('Content-Type', 'text/event-stream')
-  res.setHeader('Transfer-Encoding', 'chunked')
+    const heartbeat = setInterval(() => res.write(':thump\n'), 15000)
 
-  const heartbeat = setInterval(() => res.write(':thump\n'), 15000)
+    req.on('close', () => {
+      log.verbose(req.requestId, `Ending stream for ${req.accountId}`)
+      clearInterval(heartbeat)
+    })
 
-  req.on('close', () => {
-    log.verbose(req.requestId, `Ending stream for ${req.accountId}`)
-    clearInterval(heartbeat)
-  })
+    return (event, payload) => {
+      res.write(`event: ${event}\n`)
+      res.write(`data: ${payload}\n\n`)
+    }
+  }
 
-  return (event, payload) => {
-    res.write(`event: ${event}\n`)
-    res.write(`data: ${payload}\n\n`)
+  // Setup stream end for HTTP
+  const streamHttpEnd = req => (id, listener) => {
+    req.on('close', () => {
+      unsubscribe(id, listener)
+    })
   }
-}
 
-// Setup stream end for HTTP
-const streamHttpEnd = req => (id, listener) => {
-  req.on('close', () => {
-    unsubscribe(id, listener)
-  })
-}
+  // Setup stream output to WebSockets
+  const streamToWs = (req, ws) => {
+    const heartbeat = setInterval(() => ws.ping(), 15000)
 
-// Setup stream output to WebSockets
-const streamToWs = (req, ws) => {
-  const heartbeat = setInterval(() => ws.ping(), 15000)
+    ws.on('close', () => {
+      log.verbose(req.requestId, `Ending stream for ${req.accountId}`)
+      clearInterval(heartbeat)
+    })
 
-  ws.on('close', () => {
-    log.verbose(req.requestId, `Ending stream for ${req.accountId}`)
-    clearInterval(heartbeat)
-  })
+    return (event, payload) => {
+      if (ws.readyState !== ws.OPEN) {
+        log.error(req.requestId, 'Tried writing to closed socket')
+        return
+      }
 
-  return (event, payload) => {
-    if (ws.readyState !== ws.OPEN) {
-      log.error(req.requestId, 'Tried writing to closed socket')
-      return
+      ws.send(JSON.stringify({ event, payload }))
     }
-
-    ws.send(JSON.stringify({ event, payload }))
   }
-}
 
-// Setup stream end for WebSockets
-const streamWsEnd = ws => (id, listener) => {
-  ws.on('close', () => {
-    unsubscribe(id, listener)
-  })
+  // Setup stream end for WebSockets
+  const streamWsEnd = ws => (id, listener) => {
+    ws.on('close', () => {
+      unsubscribe(id, listener)
+    })
 
-  ws.on('error', e => {
-    unsubscribe(id, listener)
-  })
-}
+    ws.on('error', e => {
+      unsubscribe(id, listener)
+    })
+  }
 
-app.use(setRequestId)
-app.use(allowCrossDomain)
-app.use(authenticationMiddleware)
-app.use(errorMiddleware)
+  app.use(setRequestId)
+  app.use(allowCrossDomain)
+  app.use(authenticationMiddleware)
+  app.use(errorMiddleware)
 
-app.get('/api/v1/streaming/user', (req, res) => {
-  streamFrom(`timeline:${req.accountId}`, req, streamToHttp(req, res), streamHttpEnd(req))
-})
+  app.get('/api/v1/streaming/user', (req, res) => {
+    streamFrom(`timeline:${req.accountId}`, req, streamToHttp(req, res), streamHttpEnd(req))
+  })
 
-app.get('/api/v1/streaming/public', (req, res) => {
-  streamFrom('timeline:public', req, streamToHttp(req, res), streamHttpEnd(req), true)
-})
+  app.get('/api/v1/streaming/public', (req, res) => {
+    streamFrom('timeline:public', req, streamToHttp(req, res), streamHttpEnd(req), true)
+  })
 
-app.get('/api/v1/streaming/public/local', (req, res) => {
-  streamFrom('timeline:public:local', req, streamToHttp(req, res), streamHttpEnd(req), true)
-})
+  app.get('/api/v1/streaming/public/local', (req, res) => {
+    streamFrom('timeline:public:local', req, streamToHttp(req, res), streamHttpEnd(req), true)
+  })
 
-app.get('/api/v1/streaming/hashtag', (req, res) => {
-  streamFrom(`timeline:hashtag:${req.params.tag}`, req, streamToHttp(req, res), streamHttpEnd(req), true)
-})
+  app.get('/api/v1/streaming/hashtag', (req, res) => {
+    streamFrom(`timeline:hashtag:${req.params.tag}`, req, streamToHttp(req, res), streamHttpEnd(req), true)
+  })
 
-app.get('/api/v1/streaming/hashtag/local', (req, res) => {
-  streamFrom(`timeline:hashtag:${req.params.tag}:local`, req, streamToHttp(req, res), streamHttpEnd(req), true)
-})
+  app.get('/api/v1/streaming/hashtag/local', (req, res) => {
+    streamFrom(`timeline:hashtag:${req.params.tag}:local`, req, streamToHttp(req, res), streamHttpEnd(req), true)
+  })
 
-wss.on('connection', ws => {
-  const location = url.parse(ws.upgradeReq.url, true)
-  const token    = location.query.access_token
-  const req      = { requestId: uuid.v4() }
+  wss.on('connection', ws => {
+    const location = url.parse(ws.upgradeReq.url, true)
+    const token    = location.query.access_token
+    const req      = { requestId: uuid.v4() }
 
-  accountFromToken(token, req, err => {
-    if (err) {
-      log.error(req.requestId, err)
-      ws.close()
-      return
-    }
+    accountFromToken(token, req, err => {
+      if (err) {
+        log.error(req.requestId, err)
+        ws.close()
+        return
+      }
 
-    switch(location.query.stream) {
-    case 'user':
-      streamFrom(`timeline:${req.accountId}`, req, streamToWs(req, ws), streamWsEnd(ws))
-      break;
-    case 'public':
-      streamFrom('timeline:public', req, streamToWs(req, ws), streamWsEnd(ws), true)
-      break;
-    case 'public:local':
-      streamFrom('timeline:public:local', req, streamToWs(req, ws), streamWsEnd(ws), true)
-      break;
-    case 'hashtag':
-      streamFrom(`timeline:hashtag:${location.query.tag}`, req, streamToWs(req, ws), streamWsEnd(ws), true)
-      break;
-    case 'hashtag:local':
-      streamFrom(`timeline:hashtag:${location.query.tag}:local`, req, streamToWs(req, ws), streamWsEnd(ws), true)
-      break;
-    default:
-      ws.close()
-    }
+      switch(location.query.stream) {
+      case 'user':
+        streamFrom(`timeline:${req.accountId}`, req, streamToWs(req, ws), streamWsEnd(ws))
+        break;
+      case 'public':
+        streamFrom('timeline:public', req, streamToWs(req, ws), streamWsEnd(ws), true)
+        break;
+      case 'public:local':
+        streamFrom('timeline:public:local', req, streamToWs(req, ws), streamWsEnd(ws), true)
+        break;
+      case 'hashtag':
+        streamFrom(`timeline:hashtag:${location.query.tag}`, req, streamToWs(req, ws), streamWsEnd(ws), true)
+        break;
+      case 'hashtag:local':
+        streamFrom(`timeline:hashtag:${location.query.tag}:local`, req, streamToWs(req, ws), streamWsEnd(ws), true)
+        break;
+      default:
+        ws.close()
+      }
+    })
   })
-})
 
-server.listen(process.env.PORT || 4000, () => {
-  log.level = process.env.LOG_LEVEL || 'verbose'
-  log.info(`Starting streaming API server on port ${server.address().port}`)
-})
+  server.listen(process.env.PORT || 4000, () => {
+    log.level = process.env.LOG_LEVEL || 'verbose'
+    log.info(`Starting streaming API server worker on port ${server.address().port}`)
+  })
+}