about summary refs log tree commit diff
path: root/app/javascript
diff options
context:
space:
mode:
authorStarfall <us@starfall.systems>2022-11-14 18:33:13 -0600
committerStarfall <us@starfall.systems>2022-11-14 18:33:13 -0600
commitefa1bd9451c2aac68ce4576abdbbcdc266291f0f (patch)
tree4cfb9e485a912558e21032a77674562bf3fea635 /app/javascript
parentb07b6b9f339b604f9af150eb10ac1486eca8f189 (diff)
parentceafd1f68f1f1b57c998adf693678059f9aaa70c (diff)
Merge remote-tracking branch 'glitch/main'
Diffstat (limited to 'app/javascript')
-rw-r--r--app/javascript/flavours/glitch/actions/filters.js2
-rw-r--r--app/javascript/flavours/glitch/features/account/components/header.js2
-rw-r--r--app/javascript/flavours/glitch/features/emoji/emoji.js87
-rw-r--r--app/javascript/flavours/glitch/reducers/compose.js2
-rw-r--r--app/javascript/flavours/glitch/styles/components/accounts.scss12
-rw-r--r--app/javascript/flavours/glitch/styles/tables.scss3
-rw-r--r--app/javascript/mastodon/actions/filters.js2
-rw-r--r--app/javascript/mastodon/components/account.js6
-rw-r--r--app/javascript/mastodon/components/admin/Trends.js2
-rw-r--r--app/javascript/mastodon/components/hashtag.js10
-rw-r--r--app/javascript/mastodon/components/icon.js2
-rw-r--r--app/javascript/mastodon/components/permalink.js40
-rw-r--r--app/javascript/mastodon/components/status.js8
-rw-r--r--app/javascript/mastodon/components/status_content.js35
-rw-r--r--app/javascript/mastodon/features/account/components/featured_tags.js1
-rw-r--r--app/javascript/mastodon/features/account/components/header.js2
-rw-r--r--app/javascript/mastodon/features/account_gallery/components/media_item.js2
-rw-r--r--app/javascript/mastodon/features/account_timeline/components/moved_note.js8
-rw-r--r--app/javascript/mastodon/features/compose/components/navigation_bar.js10
-rw-r--r--app/javascript/mastodon/features/compose/components/reply_indicator.js2
-rw-r--r--app/javascript/mastodon/features/direct_timeline/components/conversation.js4
-rw-r--r--app/javascript/mastodon/features/directory/components/account_card.js6
-rw-r--r--app/javascript/mastodon/features/emoji/__tests__/emoji-test.js42
-rw-r--r--app/javascript/mastodon/features/emoji/emoji.js87
-rw-r--r--app/javascript/mastodon/features/follow_recommendations/components/account.js6
-rw-r--r--app/javascript/mastodon/features/follow_requests/components/account_authorize.js6
-rw-r--r--app/javascript/mastodon/features/notifications/components/follow_request.js6
-rw-r--r--app/javascript/mastodon/features/notifications/components/notification.js6
-rw-r--r--app/javascript/mastodon/features/picture_in_picture/components/footer.js2
-rw-r--r--app/javascript/mastodon/features/status/components/detailed_status.js4
-rw-r--r--app/javascript/mastodon/features/ui/components/boost_modal.js4
-rw-r--r--app/javascript/mastodon/features/ui/components/header.js5
-rw-r--r--app/javascript/mastodon/locales/ca.json72
-rw-r--r--app/javascript/mastodon/locales/cy.json14
-rw-r--r--app/javascript/mastodon/locales/de.json22
-rw-r--r--app/javascript/mastodon/locales/eo.json34
-rw-r--r--app/javascript/mastodon/locales/es-AR.json10
-rw-r--r--app/javascript/mastodon/locales/es.json2
-rw-r--r--app/javascript/mastodon/locales/fi.json4
-rw-r--r--app/javascript/mastodon/locales/gl.json4
-rw-r--r--app/javascript/mastodon/locales/ja.json6
-rw-r--r--app/javascript/mastodon/locales/lv.json10
-rw-r--r--app/javascript/mastodon/locales/ml.json6
-rw-r--r--app/javascript/mastodon/locales/nl.json4
-rw-r--r--app/javascript/mastodon/locales/nn.json2
-rw-r--r--app/javascript/mastodon/locales/no.json26
-rw-r--r--app/javascript/mastodon/locales/pl.json4
-rw-r--r--app/javascript/mastodon/locales/sk.json80
-rw-r--r--app/javascript/mastodon/locales/sq.json6
-rw-r--r--app/javascript/mastodon/locales/th.json14
-rw-r--r--app/javascript/mastodon/locales/tr.json2
-rw-r--r--app/javascript/mastodon/reducers/compose.js2
-rw-r--r--app/javascript/styles/mastodon/components.scss17
-rw-r--r--app/javascript/styles/mastodon/tables.scss3
54 files changed, 392 insertions, 368 deletions
diff --git a/app/javascript/flavours/glitch/actions/filters.js b/app/javascript/flavours/glitch/actions/filters.js
index 76326802e..e9c609fc8 100644
--- a/app/javascript/flavours/glitch/actions/filters.js
+++ b/app/javascript/flavours/glitch/actions/filters.js
@@ -43,7 +43,7 @@ export const fetchFilters = () => (dispatch, getState) => {
 export const createFilterStatus = (params, onSuccess, onFail) => (dispatch, getState) => {
   dispatch(createFilterStatusRequest());
 
-  api(getState).post(`/api/v1/filters/${params.filter_id}/statuses`, params).then(response => {
+  api(getState).post(`/api/v2/filters/${params.filter_id}/statuses`, params).then(response => {
     dispatch(createFilterStatusSuccess(response.data));
     if (onSuccess) onSuccess();
   }).catch(error => {
diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js
index 93831b3e7..47c074ec3 100644
--- a/app/javascript/flavours/glitch/features/account/components/header.js
+++ b/app/javascript/flavours/glitch/features/account/components/header.js
@@ -317,8 +317,6 @@ class Header extends ImmutablePureComponent {
               <Avatar account={suspended || hidden ? undefined : account} size={90} />
             </a>
 
-            <div className='spacer' />
-
             {!suspended && (
               <div className='account__header__tabs__buttons'>
                 {!hidden && (
diff --git a/app/javascript/flavours/glitch/features/emoji/emoji.js b/app/javascript/flavours/glitch/features/emoji/emoji.js
index c4e2c26f2..50a399114 100644
--- a/app/javascript/flavours/glitch/features/emoji/emoji.js
+++ b/app/javascript/flavours/glitch/features/emoji/emoji.js
@@ -19,15 +19,26 @@ const emojiFilename = (filename) => {
   return borderedEmoji.includes(filename) ? (filename + '_border') : filename;
 };
 
-const emojify = (str, customEmojis = {}) => {
-  const tagCharsWithoutEmojis = '<&';
-  const tagCharsWithEmojis = Object.keys(customEmojis).length ? '<&:' : '<&';
-  let rtn = '', tagChars = tagCharsWithEmojis, invisible = 0;
+const domParser = new DOMParser();
+
+const emojifyTextNode = (node, customEmojis) => {
+  let str = node.textContent;
+
+  const fragment = new DocumentFragment();
+
   for (;;) {
-    let match, i = 0, tag;
-    while (i < str.length && (tag = tagChars.indexOf(str[i])) === -1 && (invisible || useSystemEmojiFont || !(match = trie.search(str.slice(i))))) {
-      i += str.codePointAt(i) < 65536 ? 1 : 2;
+    let match, i = 0;
+
+    if (customEmojis === null) {
+      while (i < str.length && (useSystemEmojiFont || !(match = trie.search(str.slice(i))))) {
+        i += str.codePointAt(i) < 65536 ? 1 : 2;
+      }
+    } else {
+      while (i < str.length && str[i] !== ':' && (useSystemEmojiFont || !(match = trie.search(str.slice(i))))) {
+        i += str.codePointAt(i) < 65536 ? 1 : 2;
+      }
     }
+
     let rend, replacement = '';
     if (i === str.length) {
       break;
@@ -35,8 +46,6 @@ const emojify = (str, customEmojis = {}) => {
       if (!(() => {
         rend = str.indexOf(':', i + 1) + 1;
         if (!rend) return false; // no pair of ':'
-        const lt = str.indexOf('<', i + 1);
-        if (!(lt === -1 || lt >= rend)) return false; // tag appeared before closing ':'
         const shortname = str.slice(i, rend);
         // now got a replacee as ':shortname:'
         // if you want additional emoji handler, add statements below which set replacement and return true.
@@ -47,29 +56,6 @@ const emojify = (str, customEmojis = {}) => {
         }
         return false;
       })()) rend = ++i;
-    } else if (tag >= 0) { // <, &
-      rend = str.indexOf('>;'[tag], i + 1) + 1;
-      if (!rend) {
-        break;
-      }
-      if (tag === 0) {
-        if (invisible) {
-          if (str[i + 1] === '/') { // closing tag
-            if (!--invisible) {
-              tagChars = tagCharsWithEmojis;
-            }
-          } else if (str[rend - 2] !== '/') { // opening tag
-            invisible++;
-          }
-        } else {
-          if (str.startsWith('<span class="invisible">', i)) {
-            // avoid emojifying on invisible text
-            invisible = 1;
-            tagChars = tagCharsWithoutEmojis;
-          }
-        }
-      }
-      i = rend;
     } else if (!useSystemEmojiFont) { // matched to unicode emoji
       const { filename, shortCode } = unicodeMapping[match];
       const title = shortCode ? `:${shortCode}:` : '';
@@ -80,10 +66,43 @@ const emojify = (str, customEmojis = {}) => {
         rend += 1;
       }
     }
-    rtn += str.slice(0, i) + replacement;
+
+    fragment.append(document.createTextNode(str.slice(0, i)));
+    if (replacement) {
+      fragment.append(domParser.parseFromString(replacement, 'text/html').documentElement.getElementsByTagName('img')[0]);
+    }
+    node.textContent = str.slice(0, i);
     str = str.slice(rend);
   }
-  return rtn + str;
+
+  fragment.append(document.createTextNode(str));
+  node.parentElement.replaceChild(fragment, node);
+};
+
+const emojifyNode = (node, customEmojis) => {
+  for (const child of node.childNodes) {
+    switch(child.nodeType) {
+    case Node.TEXT_NODE:
+      emojifyTextNode(child, customEmojis);
+      break;
+    case Node.ELEMENT_NODE:
+      if (!child.classList.contains('invisible'))
+        emojifyNode(child, customEmojis);
+      break;
+    }
+  }
+};
+
+const emojify = (str, customEmojis = {}) => {
+  const wrapper = document.createElement('div');
+  wrapper.innerHTML = str;
+
+  if (!Object.keys(customEmojis).length)
+    customEmojis = null;
+
+  emojifyNode(wrapper, customEmojis);
+
+  return wrapper.innerHTML;
 };
 
 export default emojify;
diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js
index 18e437bbc..1edc70add 100644
--- a/app/javascript/flavours/glitch/reducers/compose.js
+++ b/app/javascript/flavours/glitch/reducers/compose.js
@@ -547,7 +547,7 @@ export default function compose(state = initialState, action) {
       .setIn(['media_modal', 'dirty'], false)
       .update('media_attachments', list => list.map(item => {
         if (item.get('id') === action.media.id) {
-          return fromJS(action.media);
+          return fromJS(action.media).set('unattached', true);
         }
 
         return item;
diff --git a/app/javascript/flavours/glitch/styles/components/accounts.scss b/app/javascript/flavours/glitch/styles/components/accounts.scss
index 00519adf1..ac2d642a8 100644
--- a/app/javascript/flavours/glitch/styles/components/accounts.scss
+++ b/app/javascript/flavours/glitch/styles/components/accounts.scss
@@ -535,8 +535,11 @@
   &__tabs {
     display: flex;
     align-items: flex-start;
+    justify-content: space-between;
     padding: 7px 10px;
     margin-top: -55px;
+    gap: 8px;
+    overflow: hidden;
 
     &__buttons {
       display: flex;
@@ -545,6 +548,15 @@
       padding-top: 55px;
       overflow: hidden;
 
+      .button {
+        flex-shrink: 1;
+        white-space: nowrap;
+
+        @media screen and (max-width: $no-gap-breakpoint) {
+          min-width: 0;
+        }
+      }
+
       .icon-button {
         border: 1px solid lighten($ui-base-color, 12%);
         border-radius: 4px;
diff --git a/app/javascript/flavours/glitch/styles/tables.scss b/app/javascript/flavours/glitch/styles/tables.scss
index 919255790..14daf591e 100644
--- a/app/javascript/flavours/glitch/styles/tables.scss
+++ b/app/javascript/flavours/glitch/styles/tables.scss
@@ -178,6 +178,9 @@ a.table-action-link {
   }
 
   &__toolbar {
+    position: sticky;
+    top: 0;
+    z-index: 1;
     border: 1px solid darken($ui-base-color, 8%);
     background: $ui-base-color;
     border-radius: 4px 0 0;
diff --git a/app/javascript/mastodon/actions/filters.js b/app/javascript/mastodon/actions/filters.js
index 76326802e..e9c609fc8 100644
--- a/app/javascript/mastodon/actions/filters.js
+++ b/app/javascript/mastodon/actions/filters.js
@@ -43,7 +43,7 @@ export const fetchFilters = () => (dispatch, getState) => {
 export const createFilterStatus = (params, onSuccess, onFail) => (dispatch, getState) => {
   dispatch(createFilterStatusRequest());
 
-  api(getState).post(`/api/v1/filters/${params.filter_id}/statuses`, params).then(response => {
+  api(getState).post(`/api/v2/filters/${params.filter_id}/statuses`, params).then(response => {
     dispatch(createFilterStatusSuccess(response.data));
     if (onSuccess) onSuccess();
   }).catch(error => {
diff --git a/app/javascript/mastodon/components/account.js b/app/javascript/mastodon/components/account.js
index 51d2b8ba2..7aebb124c 100644
--- a/app/javascript/mastodon/components/account.js
+++ b/app/javascript/mastodon/components/account.js
@@ -3,13 +3,13 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
 import Avatar from './avatar';
 import DisplayName from './display_name';
-import Permalink from './permalink';
 import IconButton from './icon_button';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import { me } from '../initial_state';
 import RelativeTimestamp from './relative_timestamp';
 import Skeleton from 'mastodon/components/skeleton';
+import { Link } from 'react-router-dom';
 
 const messages = defineMessages({
   follow: { id: 'account.follow', defaultMessage: 'Follow' },
@@ -140,11 +140,11 @@ class Account extends ImmutablePureComponent {
     return (
       <div className='account'>
         <div className='account__wrapper'>
-          <Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}>
+          <Link key={account.get('id')} className='account__display-name' title={account.get('acct')} to={`/@${account.get('acct')}`}>
             <div className='account__avatar-wrapper'><Avatar account={account} size={size} /></div>
             {mute_expires_at}
             <DisplayName account={account} />
-          </Permalink>
+          </Link>
 
           <div className='account__relationship'>
             {buttons}
diff --git a/app/javascript/mastodon/components/admin/Trends.js b/app/javascript/mastodon/components/admin/Trends.js
index 635bdf37d..9530c2a5b 100644
--- a/app/javascript/mastodon/components/admin/Trends.js
+++ b/app/javascript/mastodon/components/admin/Trends.js
@@ -50,7 +50,7 @@ export default class Trends extends React.PureComponent {
             <Hashtag
               key={hashtag.name}
               name={hashtag.name}
-              href={`/admin/tags/${hashtag.id}`}
+              to={`/admin/tags/${hashtag.id}`}
               people={hashtag.history[0].accounts * 1 + hashtag.history[1].accounts * 1}
               uses={hashtag.history[0].uses * 1 + hashtag.history[1].uses * 1}
               history={hashtag.history.reverse().map(day => day.uses)}
diff --git a/app/javascript/mastodon/components/hashtag.js b/app/javascript/mastodon/components/hashtag.js
index 75220211e..e516fc086 100644
--- a/app/javascript/mastodon/components/hashtag.js
+++ b/app/javascript/mastodon/components/hashtag.js
@@ -4,7 +4,7 @@ import { Sparklines, SparklinesCurve } from 'react-sparklines';
 import { FormattedMessage } from 'react-intl';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import Permalink from './permalink';
+import { Link } from 'react-router-dom';
 import ShortNumber from 'mastodon/components/short_number';
 import Skeleton from 'mastodon/components/skeleton';
 import classNames from 'classnames';
@@ -53,7 +53,6 @@ export const accountsCountRenderer = (displayNumber, pluralReady) => (
 export const ImmutableHashtag = ({ hashtag }) => (
   <Hashtag
     name={hashtag.get('name')}
-    href={hashtag.get('url')}
     to={`/tags/${hashtag.get('name')}`}
     people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1}
     history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()}
@@ -64,12 +63,12 @@ ImmutableHashtag.propTypes = {
   hashtag: ImmutablePropTypes.map.isRequired,
 };
 
-const Hashtag = ({ name, href, to, people, uses, history, className, description, withGraph }) => (
+const Hashtag = ({ name, to, people, uses, history, className, description, withGraph }) => (
   <div className={classNames('trends__item', className)}>
     <div className='trends__item__name'>
-      <Permalink href={href} to={to}>
+      <Link to={to}>
         {name ? <React.Fragment>#<span>{name}</span></React.Fragment> : <Skeleton width={50} />}
-      </Permalink>
+      </Link>
 
       {description ? (
         <span>{description}</span>
@@ -98,7 +97,6 @@ const Hashtag = ({ name, href, to, people, uses, history, className, description
 
 Hashtag.propTypes = {
   name: PropTypes.string,
-  href: PropTypes.string,
   to: PropTypes.string,
   people: PropTypes.number,
   description: PropTypes.node,
diff --git a/app/javascript/mastodon/components/icon.js b/app/javascript/mastodon/components/icon.js
index d8a17722f..d3d7c591d 100644
--- a/app/javascript/mastodon/components/icon.js
+++ b/app/javascript/mastodon/components/icon.js
@@ -14,7 +14,7 @@ export default class Icon extends React.PureComponent {
     const { id, className, fixedWidth, ...other } = this.props;
 
     return (
-      <i role='img' className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })} {...other} />
+      <i className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })} {...other} />
     );
   }
 
diff --git a/app/javascript/mastodon/components/permalink.js b/app/javascript/mastodon/components/permalink.js
deleted file mode 100644
index b369e9812..000000000
--- a/app/javascript/mastodon/components/permalink.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-export default class Permalink extends React.PureComponent {
-
-  static contextTypes = {
-    router: PropTypes.object,
-  };
-
-  static propTypes = {
-    className: PropTypes.string,
-    href: PropTypes.string.isRequired,
-    to: PropTypes.string.isRequired,
-    children: PropTypes.node,
-    onInterceptClick: PropTypes.func,
-  };
-
-  handleClick = e => {
-    if (this.props.onInterceptClick && this.props.onInterceptClick()) {
-      e.preventDefault();
-      return;
-    }
-
-    if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
-      e.preventDefault();
-      this.context.router.history.push(this.props.to);
-    }
-  }
-
-  render () {
-    const { href, children, className, onInterceptClick, ...other } = this.props;
-
-    return (
-      <a target='_blank' href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}>
-        {children}
-      </a>
-    );
-  }
-
-}
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js
index d1235550f..a1384ba58 100644
--- a/app/javascript/mastodon/components/status.js
+++ b/app/javascript/mastodon/components/status.js
@@ -378,7 +378,7 @@ class Status extends ImmutablePureComponent {
       prepend = (
         <div className='status__prepend'>
           <div className='status__prepend-icon-wrapper'><Icon id='retweet' className='status__prepend-icon' fixedWidth /></div>
-          <FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handlePrependAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
+          <FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handlePrependAccountClick} data-id={status.getIn(['account', 'id'])} href={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
         </div>
       );
 
@@ -392,7 +392,7 @@ class Status extends ImmutablePureComponent {
       prepend = (
         <div className='status__prepend'>
           <div className='status__prepend-icon-wrapper'><Icon id='reply' className='status__prepend-icon' fixedWidth /></div>
-          <FormattedMessage id='status.replied_to' defaultMessage='Replied to {name}' values={{ name: <a onClick={this.handlePrependAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
+          <FormattedMessage id='status.replied_to' defaultMessage='Replied to {name}' values={{ name: <a onClick={this.handlePrependAccountClick} data-id={status.getIn(['account', 'id'])} href={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
         </div>
       );
     }
@@ -511,12 +511,12 @@ class Status extends ImmutablePureComponent {
 
           <div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted })} data-id={status.get('id')}>
             <div className='status__info'>
-              <a onClick={this.handleClick} href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
+              <a onClick={this.handleClick} href={`/@${status.getIn(['account', 'acct'])}\/${status.get('id')}`} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
                 <span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span>
                 <RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
               </a>
 
-              <a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
+              <a onClick={this.handleAccountClick} href={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
                 <div className='status__avatar'>
                   {statusAvatar}
                 </div>
diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js
index 139e8ed16..fbc66eabf 100644
--- a/app/javascript/mastodon/components/status_content.js
+++ b/app/javascript/mastodon/components/status_content.js
@@ -2,13 +2,13 @@ import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
 import { FormattedMessage, injectIntl } from 'react-intl';
-import Permalink from './permalink';
+import { Link } from 'react-router-dom';
 import classnames from 'classnames';
 import PollContainer from 'mastodon/containers/poll_container';
 import Icon from 'mastodon/components/icon';
 import { autoPlayGif, languages as preloadedLanguages, translationEnabled } from 'mastodon/initial_state';
 
-const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top)
+const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
 
 class TranslateButton extends React.PureComponent {
 
@@ -77,38 +77,45 @@ class StatusContent extends React.PureComponent {
       return;
     }
 
+    const { status, onCollapsedToggle } = this.props;
     const links = node.querySelectorAll('a');
 
+    let link, mention;
+
     for (var i = 0; i < links.length; ++i) {
-      let link = links[i];
+      link = links[i];
+
       if (link.classList.contains('status-link')) {
         continue;
       }
+
       link.classList.add('status-link');
 
-      let mention = this.props.status.get('mentions').find(item => link.href === item.get('url'));
+      mention = this.props.status.get('mentions').find(item => link.href === item.get('url'));
 
       if (mention) {
         link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
         link.setAttribute('title', mention.get('acct'));
+        link.setAttribute('href', `/@${mention.get('acct')}`);
       } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
         link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
+        link.setAttribute('href', `/tags/${link.text.slice(1)}`);
       } else {
         link.setAttribute('title', link.href);
         link.classList.add('unhandled-link');
       }
     }
 
-    if (this.props.status.get('collapsed', null) === null) {
-      let collapsed =
-          this.props.collapsable
-          && this.props.onClick
-          && node.clientHeight > MAX_HEIGHT
-          && this.props.status.get('spoiler_text').length === 0;
+    if (status.get('collapsed', null) === null && onCollapsedToggle) {
+      const { collapsable, onClick } = this.props;
 
-      if(this.props.onCollapsedToggle) this.props.onCollapsedToggle(collapsed);
+      const collapsed =
+          collapsable
+          && onClick
+          && node.clientHeight > MAX_HEIGHT
+          && status.get('spoiler_text').length === 0;
 
-      this.props.status.set('collapsed', collapsed);
+      onCollapsedToggle(collapsed);
     }
   }
 
@@ -242,9 +249,9 @@ class StatusContent extends React.PureComponent {
       let mentionsPlaceholder = '';
 
       const mentionLinks = status.get('mentions').map(item => (
-        <Permalink to={`/@${item.get('acct')}`} href={item.get('url')} key={item.get('id')} className='mention'>
+        <Link to={`/@${item.get('acct')}`} key={item.get('id')} className='mention'>
           @<span>{item.get('username')}</span>
-        </Permalink>
+        </Link>
       )).reduce((aggregate, item) => [...aggregate, item, ' '], []);
 
       const toggleText = hidden ? <FormattedMessage id='status.show_more' defaultMessage='Show more' /> : <FormattedMessage id='status.show_less' defaultMessage='Show less' />;
diff --git a/app/javascript/mastodon/features/account/components/featured_tags.js b/app/javascript/mastodon/features/account/components/featured_tags.js
index 8194c063a..24a3f2171 100644
--- a/app/javascript/mastodon/features/account/components/featured_tags.js
+++ b/app/javascript/mastodon/features/account/components/featured_tags.js
@@ -39,7 +39,6 @@ class FeaturedTags extends ImmutablePureComponent {
           <Hashtag
             key={featuredTag.get('name')}
             name={featuredTag.get('name')}
-            href={featuredTag.get('url')}
             to={`/@${account.get('acct')}/tagged/${featuredTag.get('name')}`}
             uses={featuredTag.get('statuses_count') * 1}
             withGraph={false}
diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js
index c38eea55b..1825e0de6 100644
--- a/app/javascript/mastodon/features/account/components/header.js
+++ b/app/javascript/mastodon/features/account/components/header.js
@@ -314,8 +314,6 @@ class Header extends ImmutablePureComponent {
               <Avatar account={suspended || hidden ? undefined : account} size={90} />
             </a>
 
-            <div className='spacer' />
-
             {!suspended && (
               <div className='account__header__tabs__buttons'>
                 {!hidden && (
diff --git a/app/javascript/mastodon/features/account_gallery/components/media_item.js b/app/javascript/mastodon/features/account_gallery/components/media_item.js
index ba7ec46a3..3190caeb4 100644
--- a/app/javascript/mastodon/features/account_gallery/components/media_item.js
+++ b/app/javascript/mastodon/features/account_gallery/components/media_item.js
@@ -129,7 +129,7 @@ export default class MediaItem extends ImmutablePureComponent {
 
     return (
       <div className='account-gallery__item' style={{ width, height }}>
-        <a className='media-gallery__item-thumbnail' href={status.get('url')} onClick={this.handleClick} title={title} target='_blank' rel='noopener noreferrer'>
+        <a className='media-gallery__item-thumbnail' href={`/@${status.getIn(['account', 'acct'])}\/${status.get('id')}`} onClick={this.handleClick} title={title} target='_blank' rel='noopener noreferrer'>
           <Blurhash
             hash={attachment.get('blurhash')}
             className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })}
diff --git a/app/javascript/mastodon/features/account_timeline/components/moved_note.js b/app/javascript/mastodon/features/account_timeline/components/moved_note.js
index a548160a5..daff47a9d 100644
--- a/app/javascript/mastodon/features/account_timeline/components/moved_note.js
+++ b/app/javascript/mastodon/features/account_timeline/components/moved_note.js
@@ -4,7 +4,7 @@ import { FormattedMessage } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import AvatarOverlay from '../../../components/avatar_overlay';
 import DisplayName from '../../../components/display_name';
-import Permalink from 'mastodon/components/permalink';
+import { Link } from 'react-router-dom';
 
 export default class MovedNote extends ImmutablePureComponent {
 
@@ -23,12 +23,12 @@ export default class MovedNote extends ImmutablePureComponent {
         </div>
 
         <div className='moved-account-banner__action'>
-          <Permalink href={to.get('url')} to={`/@${to.get('acct')}`} className='detailed-status__display-name'>
+          <Link to={`/@${to.get('acct')}`} className='detailed-status__display-name'>
             <div className='detailed-status__display-avatar'><AvatarOverlay account={to} friend={from} /></div>
             <DisplayName account={to} />
-          </Permalink>
+          </Link>
 
-          <Permalink href={to.get('url')} to={`/@${to.get('acct')}`} className='button'><FormattedMessage id='account.go_to_profile' defaultMessage='Go to profile' /></Permalink>
+          <Link to={`/@${to.get('acct')}`} className='button'><FormattedMessage id='account.go_to_profile' defaultMessage='Go to profile' /></Link>
         </div>
       </div>
     );
diff --git a/app/javascript/mastodon/features/compose/components/navigation_bar.js b/app/javascript/mastodon/features/compose/components/navigation_bar.js
index 372765ca4..be979af50 100644
--- a/app/javascript/mastodon/features/compose/components/navigation_bar.js
+++ b/app/javascript/mastodon/features/compose/components/navigation_bar.js
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import ActionBar from './action_bar';
 import Avatar from '../../../components/avatar';
-import Permalink from '../../../components/permalink';
+import { Link } from 'react-router-dom';
 import IconButton from '../../../components/icon_button';
 import { FormattedMessage } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
@@ -19,15 +19,15 @@ export default class NavigationBar extends ImmutablePureComponent {
   render () {
     return (
       <div className='navigation-bar'>
-        <Permalink href={this.props.account.get('url')} to={`/@${this.props.account.get('acct')}`}>
+        <Link to={`/@${this.props.account.get('acct')}`}>
           <span style={{ display: 'none' }}>{this.props.account.get('acct')}</span>
           <Avatar account={this.props.account} size={46} />
-        </Permalink>
+        </Link>
 
         <div className='navigation-bar__profile'>
-          <Permalink href={this.props.account.get('url')} to={`/@${this.props.account.get('acct')}`}>
+          <Link to={`/@${this.props.account.get('acct')}`}>
             <strong className='navigation-bar__profile-account'>@{this.props.account.get('acct')}</strong>
-          </Permalink>
+          </Link>
 
           <a href='/settings/profile' className='navigation-bar__profile-edit'><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a>
         </div>
diff --git a/app/javascript/mastodon/features/compose/components/reply_indicator.js b/app/javascript/mastodon/features/compose/components/reply_indicator.js
index 863defb76..fc236882a 100644
--- a/app/javascript/mastodon/features/compose/components/reply_indicator.js
+++ b/app/javascript/mastodon/features/compose/components/reply_indicator.js
@@ -50,7 +50,7 @@ class ReplyIndicator extends ImmutablePureComponent {
         <div className='reply-indicator__header'>
           <div className='reply-indicator__cancel'><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} inverted /></div>
 
-          <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='reply-indicator__display-name'>
+          <a href={`/@${status.getIn(['account', 'acct'])}`} onClick={this.handleAccountClick} className='reply-indicator__display-name'>
             <div className='reply-indicator__display-avatar'><Avatar account={status.get('account')} size={24} /></div>
             <DisplayName account={status.get('account')} />
           </a>
diff --git a/app/javascript/mastodon/features/direct_timeline/components/conversation.js b/app/javascript/mastodon/features/direct_timeline/components/conversation.js
index 77ff2ce7b..4a770970d 100644
--- a/app/javascript/mastodon/features/direct_timeline/components/conversation.js
+++ b/app/javascript/mastodon/features/direct_timeline/components/conversation.js
@@ -7,7 +7,7 @@ import AttachmentList from 'mastodon/components/attachment_list';
 import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
 import AvatarComposite from 'mastodon/components/avatar_composite';
-import Permalink from 'mastodon/components/permalink';
+import { Link } from 'react-router-dom';
 import IconButton from 'mastodon/components/icon_button';
 import RelativeTimestamp from 'mastodon/components/relative_timestamp';
 import { HotKeys } from 'react-hotkeys';
@@ -133,7 +133,7 @@ class Conversation extends ImmutablePureComponent {
 
     menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDelete });
 
-    const names = accounts.map(a => <Permalink to={`/@${a.get('acct')}`} href={a.get('url')} key={a.get('id')} title={a.get('acct')}><bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi></Permalink>).reduce((prev, cur) => [prev, ', ', cur]);
+    const names = accounts.map(a => <Link to={`/@${a.get('acct')}`} key={a.get('id')} title={a.get('acct')}><bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi></Link>).reduce((prev, cur) => [prev, ', ', cur]);
 
     const handlers = {
       reply: this.handleReply,
diff --git a/app/javascript/mastodon/features/directory/components/account_card.js b/app/javascript/mastodon/features/directory/components/account_card.js
index e7eeb2254..977f0c32c 100644
--- a/app/javascript/mastodon/features/directory/components/account_card.js
+++ b/app/javascript/mastodon/features/directory/components/account_card.js
@@ -6,7 +6,7 @@ import { connect } from 'react-redux';
 import { makeGetAccount } from 'mastodon/selectors';
 import Avatar from 'mastodon/components/avatar';
 import DisplayName from 'mastodon/components/display_name';
-import Permalink from 'mastodon/components/permalink';
+import { Link } from 'react-router-dom';
 import Button from 'mastodon/components/button';
 import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
 import { autoPlayGif, me, unfollowModal } from 'mastodon/initial_state';
@@ -169,7 +169,7 @@ class AccountCard extends ImmutablePureComponent {
 
     return (
       <div className='account-card'>
-        <Permalink href={account.get('url')} to={`/@${account.get('acct')}`} className='account-card__permalink'>
+        <Link to={`/@${account.get('acct')}`} className='account-card__permalink'>
           <div className='account-card__header'>
             <img
               src={
@@ -183,7 +183,7 @@ class AccountCard extends ImmutablePureComponent {
             <div className='account-card__title__avatar'><Avatar account={account} size={56} /></div>
             <DisplayName account={account} />
           </div>
-        </Permalink>
+        </Link>
 
         {account.get('note').length > 0 && (
           <div
diff --git a/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js
index 07b3d8c53..2f19aab7e 100644
--- a/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js
+++ b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js
@@ -11,8 +11,8 @@ describe('emoji', () => {
     });
 
     it('works with unclosed tags', () => {
-      expect(emojify('hello>')).toEqual('hello>');
-      expect(emojify('<hello')).toEqual('<hello');
+      expect(emojify('hello>')).toEqual('hello&gt;');
+      expect(emojify('<hello')).toEqual('');
     });
 
     it('works with unclosed shortcodes', () => {
@@ -22,23 +22,23 @@ describe('emoji', () => {
 
     it('does unicode', () => {
       expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).toEqual(
-        '<img draggable="false" class="emojione" alt="👩‍👩‍👦‍👦" title=":woman-woman-boy-boy:" src="/emoji/1f469-200d-1f469-200d-1f466-200d-1f466.svg" />');
+        '<img draggable="false" class="emojione" alt="👩‍👩‍👦‍👦" title=":woman-woman-boy-boy:" src="/emoji/1f469-200d-1f469-200d-1f466-200d-1f466.svg">');
       expect(emojify('👨‍👩‍👧‍👧')).toEqual(
-        '<img draggable="false" class="emojione" alt="👨‍👩‍👧‍👧" title=":man-woman-girl-girl:" src="/emoji/1f468-200d-1f469-200d-1f467-200d-1f467.svg" />');
-      expect(emojify('👩‍👩‍👦')).toEqual('<img draggable="false" class="emojione" alt="👩‍👩‍👦" title=":woman-woman-boy:" src="/emoji/1f469-200d-1f469-200d-1f466.svg" />');
+        '<img draggable="false" class="emojione" alt="👨‍👩‍👧‍👧" title=":man-woman-girl-girl:" src="/emoji/1f468-200d-1f469-200d-1f467-200d-1f467.svg">');
+      expect(emojify('👩‍👩‍👦')).toEqual('<img draggable="false" class="emojione" alt="👩‍👩‍👦" title=":woman-woman-boy:" src="/emoji/1f469-200d-1f469-200d-1f466.svg">');
       expect(emojify('\u2757')).toEqual(
-        '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />');
+        '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg">');
     });
 
     it('does multiple unicode', () => {
       expect(emojify('\u2757 #\uFE0F\u20E3')).toEqual(
-        '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" />');
+        '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg">');
       expect(emojify('\u2757#\uFE0F\u20E3')).toEqual(
-        '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" />');
+        '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg">');
       expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).toEqual(
-        '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" /> <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />');
+        '<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"> <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg">');
       expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).toEqual(
-        'foo <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" /> bar');
+        'foo <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"> bar');
     });
 
     it('ignores unicode inside of tags', () => {
@@ -46,16 +46,16 @@ describe('emoji', () => {
     });
 
     it('does multiple emoji properly (issue 5188)', () => {
-      expect(emojify('👌🌈💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg" /><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg" /><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg" />');
-      expect(emojify('👌 🌈 💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg" /> <img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg" /> <img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg" />');
+      expect(emojify('👌🌈💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg"><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg"><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg">');
+      expect(emojify('👌 🌈 💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg"> <img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg"> <img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg">');
     });
 
     it('does an emoji that has no shortcode', () => {
-      expect(emojify('👁‍🗨')).toEqual('<img draggable="false" class="emojione" alt="👁‍🗨" title="" src="/emoji/1f441-200d-1f5e8.svg" />');
+      expect(emojify('👁‍🗨')).toEqual('<img draggable="false" class="emojione" alt="👁‍🗨" title="" src="/emoji/1f441-200d-1f5e8.svg">');
     });
 
     it('does an emoji whose filename is irregular', () => {
-      expect(emojify('↙️')).toEqual('<img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/emoji/2199.svg" />');
+      expect(emojify('↙️')).toEqual('<img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/emoji/2199.svg">');
     });
 
     it('avoid emojifying on invisible text', () => {
@@ -67,26 +67,26 @@ describe('emoji', () => {
 
     it('avoid emojifying on invisible text with nested tags', () => {
       expect(emojify('<span class="invisible">😄<span class="foo">bar</span>😴</span>😇'))
-        .toEqual('<span class="invisible">😄<span class="foo">bar</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />');
+        .toEqual('<span class="invisible">😄<span class="foo">bar</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg">');
       expect(emojify('<span class="invisible">😄<span class="invisible">😕</span>😴</span>😇'))
-        .toEqual('<span class="invisible">😄<span class="invisible">😕</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />');
-      expect(emojify('<span class="invisible">😄<br/>😴</span>😇'))
-        .toEqual('<span class="invisible">😄<br/>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />');
+        .toEqual('<span class="invisible">😄<span class="invisible">😕</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg">');
+      expect(emojify('<span class="invisible">😄<br>😴</span>😇'))
+        .toEqual('<span class="invisible">😄<br>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg">');
     });
 
     it('skips the textual presentation VS15 character', () => {
       expect(emojify('✴︎')) // This is U+2734 EIGHT POINTED BLACK STAR then U+FE0E VARIATION SELECTOR-15
-        .toEqual('<img draggable="false" class="emojione" alt="✴" title=":eight_pointed_black_star:" src="/emoji/2734_border.svg" />');
+        .toEqual('<img draggable="false" class="emojione" alt="✴" title=":eight_pointed_black_star:" src="/emoji/2734_border.svg">');
     });
 
     it('does an simple emoji properly', () => {
       expect(emojify('♀♂'))
-        .toEqual('<img draggable="false" class="emojione" alt="♀" title=":female_sign:" src="/emoji/2640.svg" /><img draggable="false" class="emojione" alt="♂" title=":male_sign:" src="/emoji/2642.svg" />');
+        .toEqual('<img draggable="false" class="emojione" alt="♀" title=":female_sign:" src="/emoji/2640.svg"><img draggable="false" class="emojione" alt="♂" title=":male_sign:" src="/emoji/2642.svg">');
     });
 
     it('does an emoji containing ZWJ properly', () => {
       expect(emojify('💂‍♀️💂‍♂️'))
-        .toEqual('<img draggable="false" class="emojione" alt="💂\u200D♀️" title=":female-guard:" src="/emoji/1f482-200d-2640-fe0f_border.svg" /><img draggable="false" class="emojione" alt="💂\u200D♂️" title=":male-guard:" src="/emoji/1f482-200d-2642-fe0f_border.svg" />');
+        .toEqual('<img draggable="false" class="emojione" alt="💂\u200D♀️" title=":female-guard:" src="/emoji/1f482-200d-2640-fe0f_border.svg"><img draggable="false" class="emojione" alt="💂\u200D♂️" title=":male-guard:" src="/emoji/1f482-200d-2642-fe0f_border.svg">');
     });
   });
 });
diff --git a/app/javascript/mastodon/features/emoji/emoji.js b/app/javascript/mastodon/features/emoji/emoji.js
index fb1a3804c..52a8458fb 100644
--- a/app/javascript/mastodon/features/emoji/emoji.js
+++ b/app/javascript/mastodon/features/emoji/emoji.js
@@ -19,15 +19,26 @@ const emojiFilename = (filename) => {
   return borderedEmoji.includes(filename) ? (filename + '_border') : filename;
 };
 
-const emojify = (str, customEmojis = {}) => {
-  const tagCharsWithoutEmojis = '<&';
-  const tagCharsWithEmojis = Object.keys(customEmojis).length ? '<&:' : '<&';
-  let rtn = '', tagChars = tagCharsWithEmojis, invisible = 0;
+const domParser = new DOMParser();
+
+const emojifyTextNode = (node, customEmojis) => {
+  let str = node.textContent;
+
+  const fragment = new DocumentFragment();
+
   for (;;) {
-    let match, i = 0, tag;
-    while (i < str.length && (tag = tagChars.indexOf(str[i])) === -1 && (invisible || !(match = trie.search(str.slice(i))))) {
-      i += str.codePointAt(i) < 65536 ? 1 : 2;
+    let match, i = 0;
+
+    if (customEmojis === null) {
+      while (i < str.length && !(match = trie.search(str.slice(i)))) {
+        i += str.codePointAt(i) < 65536 ? 1 : 2;
+      }
+    } else {
+      while (i < str.length && str[i] !== ':' && !(match = trie.search(str.slice(i)))) {
+        i += str.codePointAt(i) < 65536 ? 1 : 2;
+      }
     }
+
     let rend, replacement = '';
     if (i === str.length) {
       break;
@@ -35,8 +46,6 @@ const emojify = (str, customEmojis = {}) => {
       if (!(() => {
         rend = str.indexOf(':', i + 1) + 1;
         if (!rend) return false; // no pair of ':'
-        const lt = str.indexOf('<', i + 1);
-        if (!(lt === -1 || lt >= rend)) return false; // tag appeared before closing ':'
         const shortname = str.slice(i, rend);
         // now got a replacee as ':shortname:'
         // if you want additional emoji handler, add statements below which set replacement and return true.
@@ -47,29 +56,6 @@ const emojify = (str, customEmojis = {}) => {
         }
         return false;
       })()) rend = ++i;
-    } else if (tag >= 0) { // <, &
-      rend = str.indexOf('>;'[tag], i + 1) + 1;
-      if (!rend) {
-        break;
-      }
-      if (tag === 0) {
-        if (invisible) {
-          if (str[i + 1] === '/') { // closing tag
-            if (!--invisible) {
-              tagChars = tagCharsWithEmojis;
-            }
-          } else if (str[rend - 2] !== '/') { // opening tag
-            invisible++;
-          }
-        } else {
-          if (str.startsWith('<span class="invisible">', i)) {
-            // avoid emojifying on invisible text
-            invisible = 1;
-            tagChars = tagCharsWithoutEmojis;
-          }
-        }
-      }
-      i = rend;
     } else { // matched to unicode emoji
       const { filename, shortCode } = unicodeMapping[match];
       const title = shortCode ? `:${shortCode}:` : '';
@@ -80,10 +66,43 @@ const emojify = (str, customEmojis = {}) => {
         rend += 1;
       }
     }
-    rtn += str.slice(0, i) + replacement;
+
+    fragment.append(document.createTextNode(str.slice(0, i)));
+    if (replacement) {
+      fragment.append(domParser.parseFromString(replacement, 'text/html').documentElement.getElementsByTagName('img')[0]);
+    }
+    node.textContent = str.slice(0, i);
     str = str.slice(rend);
   }
-  return rtn + str;
+
+  fragment.append(document.createTextNode(str));
+  node.parentElement.replaceChild(fragment, node);
+};
+
+const emojifyNode = (node, customEmojis) => {
+  for (const child of node.childNodes) {
+    switch(child.nodeType) {
+    case Node.TEXT_NODE:
+      emojifyTextNode(child, customEmojis);
+      break;
+    case Node.ELEMENT_NODE:
+      if (!child.classList.contains('invisible'))
+        emojifyNode(child, customEmojis);
+      break;
+    }
+  }
+};
+
+const emojify = (str, customEmojis = {}) => {
+  const wrapper = document.createElement('div');
+  wrapper.innerHTML = str;
+
+  if (!Object.keys(customEmojis).length)
+    customEmojis = null;
+
+  emojifyNode(wrapper, customEmojis);
+
+  return wrapper.innerHTML;
 };
 
 export default emojify;
diff --git a/app/javascript/mastodon/features/follow_recommendations/components/account.js b/app/javascript/mastodon/features/follow_recommendations/components/account.js
index ffc0ab00c..14f4e7e1b 100644
--- a/app/javascript/mastodon/features/follow_recommendations/components/account.js
+++ b/app/javascript/mastodon/features/follow_recommendations/components/account.js
@@ -6,7 +6,7 @@ import { connect } from 'react-redux';
 import { makeGetAccount } from 'mastodon/selectors';
 import Avatar from 'mastodon/components/avatar';
 import DisplayName from 'mastodon/components/display_name';
-import Permalink from 'mastodon/components/permalink';
+import { Link } from 'react-router-dom';
 import IconButton from 'mastodon/components/icon_button';
 import { injectIntl, defineMessages } from 'react-intl';
 import { followAccount, unfollowAccount } from 'mastodon/actions/accounts';
@@ -66,13 +66,13 @@ class Account extends ImmutablePureComponent {
     return (
       <div className='account follow-recommendations-account'>
         <div className='account__wrapper'>
-          <Permalink className='account__display-name account__display-name--with-note' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}>
+          <Link className='account__display-name account__display-name--with-note' title={account.get('acct')} to={`/@${account.get('acct')}`}>
             <div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
 
             <DisplayName account={account} />
 
             <div className='account__note'>{getFirstSentence(account.get('note_plain'))}</div>
-          </Permalink>
+          </Link>
 
           <div className='account__relationship'>
             {button}
diff --git a/app/javascript/mastodon/features/follow_requests/components/account_authorize.js b/app/javascript/mastodon/features/follow_requests/components/account_authorize.js
index 263a7ae16..d41f331e5 100644
--- a/app/javascript/mastodon/features/follow_requests/components/account_authorize.js
+++ b/app/javascript/mastodon/features/follow_requests/components/account_authorize.js
@@ -1,7 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import Permalink from '../../../components/permalink';
+import { Link } from 'react-router-dom';
 import Avatar from '../../../components/avatar';
 import DisplayName from '../../../components/display_name';
 import IconButton from '../../../components/icon_button';
@@ -30,10 +30,10 @@ class AccountAuthorize extends ImmutablePureComponent {
     return (
       <div className='account-authorize__wrapper'>
         <div className='account-authorize'>
-          <Permalink href={account.get('url')} to={`/@${account.get('acct')}`} className='detailed-status__display-name'>
+          <Link to={`/@${account.get('acct')}`} className='detailed-status__display-name'>
             <div className='account-authorize__avatar'><Avatar account={account} size={48} /></div>
             <DisplayName account={account} />
-          </Permalink>
+          </Link>
 
           <div className='account__header__content translate' dangerouslySetInnerHTML={content} />
         </div>
diff --git a/app/javascript/mastodon/features/notifications/components/follow_request.js b/app/javascript/mastodon/features/notifications/components/follow_request.js
index 9ef3fde7e..08de875e3 100644
--- a/app/javascript/mastodon/features/notifications/components/follow_request.js
+++ b/app/javascript/mastodon/features/notifications/components/follow_request.js
@@ -3,7 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
 import Avatar from 'mastodon/components/avatar';
 import DisplayName from 'mastodon/components/display_name';
-import Permalink from 'mastodon/components/permalink';
+import { Link } from 'react-router-dom';
 import IconButton from 'mastodon/components/icon_button';
 import { defineMessages, injectIntl } from 'react-intl';
 import ImmutablePureComponent from 'react-immutable-pure-component';
@@ -42,10 +42,10 @@ class FollowRequest extends ImmutablePureComponent {
     return (
       <div className='account'>
         <div className='account__wrapper'>
-          <Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}>
+          <Link key={account.get('id')} className='account__display-name' title={account.get('acct')} to={`/@${account.get('acct')}`}>
             <div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
             <DisplayName account={account} />
-          </Permalink>
+          </Link>
 
           <div className='account__relationship'>
             <IconButton title={intl.formatMessage(messages.authorize)} icon='check' onClick={onAuthorize} />
diff --git a/app/javascript/mastodon/features/notifications/components/notification.js b/app/javascript/mastodon/features/notifications/components/notification.js
index 5974e378e..ea2c9c0a4 100644
--- a/app/javascript/mastodon/features/notifications/components/notification.js
+++ b/app/javascript/mastodon/features/notifications/components/notification.js
@@ -10,7 +10,7 @@ import AccountContainer from 'mastodon/containers/account_container';
 import Report from './report';
 import FollowRequestContainer from '../containers/follow_request_container';
 import Icon from 'mastodon/components/icon';
-import Permalink from 'mastodon/components/permalink';
+import { Link } from 'react-router-dom';
 import classNames from 'classnames';
 
 const messages = defineMessages({
@@ -378,7 +378,7 @@ class Notification extends ImmutablePureComponent {
 
     const targetAccount = report.get('target_account');
     const targetDisplayNameHtml = { __html: targetAccount.get('display_name_html') };
-    const targetLink = <bdi><Permalink className='notification__display-name' href={targetAccount.get('url')} title={targetAccount.get('acct')} to={`/@${targetAccount.get('acct')}`} dangerouslySetInnerHTML={targetDisplayNameHtml} /></bdi>;
+    const targetLink = <bdi><Link className='notification__display-name' title={targetAccount.get('acct')} to={`/@${targetAccount.get('acct')}`} dangerouslySetInnerHTML={targetDisplayNameHtml} /></bdi>;
 
     return (
       <HotKeys handlers={this.getHandlers()}>
@@ -403,7 +403,7 @@ class Notification extends ImmutablePureComponent {
     const { notification } = this.props;
     const account          = notification.get('account');
     const displayNameHtml  = { __html: account.get('display_name_html') };
-    const link             = <bdi><Permalink className='notification__display-name' href={account.get('url')} title={account.get('acct')} to={`/@${account.get('acct')}`} dangerouslySetInnerHTML={displayNameHtml} /></bdi>;
+    const link             = <bdi><Link className='notification__display-name' href={`/@${account.get('acct')}`} title={account.get('acct')} to={`/@${account.get('acct')}`} dangerouslySetInnerHTML={displayNameHtml} /></bdi>;
 
     switch(notification.get('type')) {
     case 'follow':
diff --git a/app/javascript/mastodon/features/picture_in_picture/components/footer.js b/app/javascript/mastodon/features/picture_in_picture/components/footer.js
index 5b875dc30..0dff834c3 100644
--- a/app/javascript/mastodon/features/picture_in_picture/components/footer.js
+++ b/app/javascript/mastodon/features/picture_in_picture/components/footer.js
@@ -184,7 +184,7 @@ class Footer extends ImmutablePureComponent {
         <IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
         <IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate}  active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} counter={status.get('reblogs_count')} />
         <IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={status.get('favourites_count')} />
-        {withOpenButton && <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.open)} icon='external-link' onClick={this.handleOpenClick} href={status.get('url')} />}
+        {withOpenButton && <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.open)} icon='external-link' onClick={this.handleOpenClick} href={`/@${status.getIn(['account', 'acct'])}\/${status.get('id')}`} />}
       </div>
     );
   }
diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js
index 1a2aab819..c62910e0e 100644
--- a/app/javascript/mastodon/features/status/components/detailed_status.js
+++ b/app/javascript/mastodon/features/status/components/detailed_status.js
@@ -261,7 +261,7 @@ class DetailedStatus extends ImmutablePureComponent {
     return (
       <div style={outerStyle}>
         <div ref={this.setRef} className={classNames('detailed-status', `detailed-status-${status.get('visibility')}`, { compact })}>
-          <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name'>
+          <a href={`/@${status.getIn(['account', 'acct'])}`} onClick={this.handleAccountClick} className='detailed-status__display-name'>
             <div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={46} /></div>
             <DisplayName account={status.get('account')} localDomain={this.props.domain} />
           </a>
@@ -276,7 +276,7 @@ class DetailedStatus extends ImmutablePureComponent {
           {media}
 
           <div className='detailed-status__meta'>
-            <a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'>
+            <a className='detailed-status__datetime' href={`/@${status.getIn(['account', 'acct'])}\/${status.get('id')}`} target='_blank' rel='noopener noreferrer'>
               <FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
             </a>{edited}{visibilityLink}{applicationLink}{reblogLink} · {favouriteLink}
           </div>
diff --git a/app/javascript/mastodon/features/ui/components/boost_modal.js b/app/javascript/mastodon/features/ui/components/boost_modal.js
index d7a6d711e..077ce7b35 100644
--- a/app/javascript/mastodon/features/ui/components/boost_modal.js
+++ b/app/javascript/mastodon/features/ui/components/boost_modal.js
@@ -98,12 +98,12 @@ class BoostModal extends ImmutablePureComponent {
         <div className='boost-modal__container'>
           <div className={classNames('status', `status-${status.get('visibility')}`, 'light')}>
             <div className='status__info'>
-              <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
+              <a href={`/@${status.getIn(['account', 'acct'])}\/${status.get('id')}`} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
                 <span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span>
                 <RelativeTimestamp timestamp={status.get('created_at')} />
               </a>
 
-              <a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} className='status__display-name'>
+              <a onClick={this.handleAccountClick} href={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name'>
                 <div className='status__avatar'>
                   <Avatar account={status.get('account')} size={48} />
                 </div>
diff --git a/app/javascript/mastodon/features/ui/components/header.js b/app/javascript/mastodon/features/ui/components/header.js
index a1c281315..4e109080e 100644
--- a/app/javascript/mastodon/features/ui/components/header.js
+++ b/app/javascript/mastodon/features/ui/components/header.js
@@ -4,16 +4,15 @@ import { Link, withRouter } from 'react-router-dom';
 import { FormattedMessage } from 'react-intl';
 import { registrationsOpen, me } from 'mastodon/initial_state';
 import Avatar from 'mastodon/components/avatar';
-import Permalink from 'mastodon/components/permalink';
 import PropTypes from 'prop-types';
 import { connect } from 'react-redux';
 
 const Account = connect(state => ({
   account: state.getIn(['accounts', me]),
 }))(({ account }) => (
-  <Permalink href={account.get('url')} to={`/@${account.get('acct')}`} title={account.get('acct')}>
+  <Link to={`/@${account.get('acct')}`} title={account.get('acct')}>
     <Avatar account={account} size={35} />
-  </Permalink>
+  </Link>
 ));
 
 export default @withRouter
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index ea63eedb3..4b5fb25e4 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -1,14 +1,14 @@
 {
   "about.blocks": "Servidors moderats",
   "about.contact": "Contacte:",
-  "about.disclaimer": "Mastodon és un programari lliure de codi obert i una marca comercial de Mastodon gGmbH.",
-  "about.domain_blocks.no_reason_available": "Motiu no disponible",
+  "about.disclaimer": "Mastodon és programari lliure de codi obert i una marca comercial de Mastodon gGmbH.",
+  "about.domain_blocks.no_reason_available": "No és disponible el motiu",
   "about.domain_blocks.preamble": "En general, Mastodon et permet veure el contingut i interaccionar amb els usuaris de qualsevol altre servidor del fedivers. Aquestes són les excepcions que s'han fet en aquest servidor particular.",
-  "about.domain_blocks.silenced.explanation": "Generalment no veuràs perfils ni contingut d'aquest servidor, a menys que el cerquis explícitament o optis per ell seguint-lo.",
+  "about.domain_blocks.silenced.explanation": "Generalment no veuràs perfils ni contingut d'aquest servidor, a menys que el cerquis explícitament o optis per seguir-lo.",
   "about.domain_blocks.silenced.title": "Limitat",
-  "about.domain_blocks.suspended.explanation": "No es processaran, emmagatzemaran ni s'intercanviaran dades d'aquest servidor, fent impossible qualsevol interacció o comunicació amb els usuaris d'aquest servidor.",
+  "about.domain_blocks.suspended.explanation": "No es processaran, emmagatzemaran ni intercanviaran dades d'aquest servidor, fent impossible qualsevol interacció o comunicació amb els seus usuaris.",
   "about.domain_blocks.suspended.title": "Suspès",
-  "about.not_available": "Aquesta informació no s'ha fet disponible en aquest servidor.",
+  "about.not_available": "Aquesta informació no és disponible en aquest servidor.",
   "about.powered_by": "Xarxa social descentralitzada impulsada per {mastodon}",
   "about.rules": "Normes del servidor",
   "account.account_note_header": "Nota",
@@ -19,19 +19,19 @@
   "account.block_domain": "Bloqueja el domini {domain}",
   "account.blocked": "Bloquejat",
   "account.browse_more_on_origin_server": "Navega més en el perfil original",
-  "account.cancel_follow_request": "Retirar la sol·licitud de seguiment",
+  "account.cancel_follow_request": "Retira la sol·licitud de seguiment",
   "account.direct": "Envia missatge directe a @{name}",
   "account.disable_notifications": "No em notifiquis les publicacions de @{name}",
-  "account.domain_blocked": "Domini bloquejat",
+  "account.domain_blocked": "Domini blocat",
   "account.edit_profile": "Edita el perfil",
-  "account.enable_notifications": "Notifica’m les publicacions de @{name}",
+  "account.enable_notifications": "Notifica'm les publicacions de @{name}",
   "account.endorse": "Recomana en el perfil",
   "account.featured_tags.last_status_at": "Última publicació el {date}",
-  "account.featured_tags.last_status_never": "Cap publicació",
+  "account.featured_tags.last_status_never": "No hi ha publicacions",
   "account.featured_tags.title": "Etiquetes destacades de: {name}",
   "account.follow": "Segueix",
   "account.followers": "Seguidors",
-  "account.followers.empty": "Ningú segueix aquest usuari encara.",
+  "account.followers.empty": "Encara ningú no segueix aquest usuari.",
   "account.followers_counter": "{count, plural, one {{counter} Seguidor} other {{counter} Seguidors}}",
   "account.following": "Seguint",
   "account.following_counter": "{count, plural, other {{counter} Seguint}}",
@@ -52,25 +52,25 @@
   "account.open_original_page": "Obre la pàgina original",
   "account.posts": "Publicacions",
   "account.posts_with_replies": "Publicacions i respostes",
-  "account.report": "Informa sobre @{name}",
-  "account.requested": "Esperant aprovació. Fes clic per cancel·lar la petició de seguiment",
+  "account.report": "Informa quant a @{name}",
+  "account.requested": "S'està esperant l'aprovació. Feu clic per a cancel·lar la petició de seguiment",
   "account.share": "Comparteix el perfil de @{name}",
   "account.show_reblogs": "Mostra els impulsos de @{name}",
   "account.statuses_counter": "{count, plural, one {{counter} Publicació} other {{counter} Publicacions}}",
   "account.unblock": "Desbloqueja @{name}",
   "account.unblock_domain": "Desbloqueja el domini {domain}",
-  "account.unblock_short": "Desbloquejar",
-  "account.unendorse": "No recomanar en el perfil",
-  "account.unfollow": "Deixar de seguir",
+  "account.unblock_short": "Desbloqueja",
+  "account.unendorse": "No recomanis en el perfil",
+  "account.unfollow": "Deixa de seguir",
   "account.unmute": "Deixar de silenciar @{name}",
-  "account.unmute_notifications": "Activar notificacions de @{name}",
+  "account.unmute_notifications": "Activa les notificacions de @{name}",
   "account.unmute_short": "Deixa de silenciar",
   "account_note.placeholder": "Clica per afegir-hi una nota",
-  "admin.dashboard.daily_retention": "Ràtio de retenció d'usuaris nous, per dia, després del registre",
-  "admin.dashboard.monthly_retention": "Ràtio de retenció d'usuaris nous, per mes, després del registre",
+  "admin.dashboard.daily_retention": "Ràtio de retenció d'usuaris nous per dia, després del registre",
+  "admin.dashboard.monthly_retention": "Ràtio de retenció d'usuaris nous per mes, després del registre",
   "admin.dashboard.retention.average": "Mitjana",
-  "admin.dashboard.retention.cohort": "Mes del registre",
-  "admin.dashboard.retention.cohort_size": "Nous usuaris",
+  "admin.dashboard.retention.cohort": "Mes de registre",
+  "admin.dashboard.retention.cohort_size": "Usuaris nous",
   "alert.rate_limited.message": "Si us plau, torna-ho a provar després de {retry_time, time, medium}.",
   "alert.rate_limited.title": "Límit de freqüència",
   "alert.unexpected.message": "S'ha produït un error inesperat.",
@@ -79,21 +79,21 @@
   "attachments_list.unprocessed": "(sense processar)",
   "audio.hide": "Amaga l'àudio",
   "autosuggest_hashtag.per_week": "{count} per setmana",
-  "boost_modal.combo": "Pots prémer {combo} per evitar-ho el pròxim cop",
-  "bundle_column_error.copy_stacktrace": "Copiar l'informe d'error",
-  "bundle_column_error.error.body": "No s'ha pogut renderitzar la pàgina sol·licitada. Podría ser degut a un error en el nostre codi o un problema de compatibilitat del navegador.",
+  "boost_modal.combo": "Podeu prémer {combo} per a evitar-ho el pròxim cop",
+  "bundle_column_error.copy_stacktrace": "Copia l'informe d'error",
+  "bundle_column_error.error.body": "No s'ha pogut renderitzar la pàgina sol·licitada. Podria ser per un error en el nostre codi o per un problema de compatibilitat del navegador.",
   "bundle_column_error.error.title": "Oh, no!",
-  "bundle_column_error.network.body": "Hi ha hagut un error al intentar carregar aquesta pàgina. Això podria ser degut a un problem temporal amb la teva connexió a internet o amb aquest servidor.",
-  "bundle_column_error.network.title": "Error de xarxa",
-  "bundle_column_error.retry": "Tornar-ho a provar",
+  "bundle_column_error.network.body": "Hi ha hagut un error en intentar carregar aquesta pàgina. Això podria ser per un problema temporal amb la teva connexió a internet o amb aquest servidor.",
+  "bundle_column_error.network.title": "Error de connexió",
+  "bundle_column_error.retry": "Torna-ho a provar",
   "bundle_column_error.return": "Torna a Inici",
-  "bundle_column_error.routing.body": "No es pot trobar la pàgina sol·licitada. Estàs segur que la URL de la barra d'adreces és correcte?",
+  "bundle_column_error.routing.body": "No es pot trobar la pàgina sol·licitada. Segur que la URL de la barra d'adreces és correcta?",
   "bundle_column_error.routing.title": "404",
   "bundle_modal_error.close": "Tanca",
   "bundle_modal_error.message": "S'ha produït un error en carregar aquest component.",
-  "bundle_modal_error.retry": "Tornar-ho a provar",
-  "closed_registrations.other_server_instructions": "Donat que Mastodon és descentralitzat, pots crear un compte en un altre servidor i encara interactuar amb aquest.",
-  "closed_registrations_modal.description": "Crear un compte a {domain} no és possible ara mateix però, si us plau, tingues en compte que no necessites específicament un compte a {domain} per a usar Mastodon.",
+  "bundle_modal_error.retry": "Torna-ho a provar",
+  "closed_registrations.other_server_instructions": "Com que Mastodon és descentralitzat, pots crear un compte en un altre servidor i seguir interactuant amb aquest.",
+  "closed_registrations_modal.description": "No es pot crear un compte a {domain} ara mateix, però tingueu en compte que no necessiteu específicament un compte a {domain} per a usar Mastodon.",
   "closed_registrations_modal.find_another_server": "Troba un altre servidor",
   "closed_registrations_modal.preamble": "Mastodon és descentralitzat per tant no importa on tinguis el teu compte, seràs capaç de seguir i interactuar amb tothom des d'aquest servidor. Fins i tot pots tenir el compte en el teu propi servidor!",
   "closed_registrations_modal.title": "Registrant-se a Mastodon",
@@ -130,7 +130,7 @@
   "compose_form.hashtag_warning": "Aquesta publicació no es mostrarà en cap etiqueta, ja que no està llistada. Només les publicacions públiques es poden cercar per etiqueta.",
   "compose_form.lock_disclaimer": "El teu compte no està {locked}. Tothom pot seguir-te i veure les publicacions de només per a seguidors.",
   "compose_form.lock_disclaimer.lock": "bloquejat",
-  "compose_form.placeholder": "Què et passa pel cap?",
+  "compose_form.placeholder": "Què tens en ment?",
   "compose_form.poll.add_option": "Afegir una opció",
   "compose_form.poll.duration": "Durada de l'enquesta",
   "compose_form.poll.option_placeholder": "Opció {number}",
@@ -210,7 +210,7 @@
   "empty_column.account_timeline": "No hi ha publicacions aquí!",
   "empty_column.account_unavailable": "Perfil no disponible",
   "empty_column.blocks": "Encara no has bloquejat cap usuari.",
-  "empty_column.bookmarked_statuses": "Encara no has marcat com publicació com a preferida. Quan en marquis una apareixerà aquí.",
+  "empty_column.bookmarked_statuses": "Encara no has marcat cap publicació com a preferida. Quan en marquis una, apareixerà aquí.",
   "empty_column.community": "La línia de temps local és buida. Escriu alguna cosa públicament per posar-ho tot en marxa!",
   "empty_column.direct": "Encara no tens missatges directes. Quan n'enviïs o en rebis, es mostraran aquí.",
   "empty_column.domain_blocks": "Encara no hi ha dominis bloquejats.",
@@ -257,7 +257,7 @@
   "filter_modal.title.status": "Filtra una publicació",
   "follow_recommendations.done": "Fet",
   "follow_recommendations.heading": "Segueix a la gent de la que t'agradaria veure les seves publicacions! Aquí hi ha algunes recomanacions.",
-  "follow_recommendations.lead": "Les publicacions del usuaris que segueixes es mostraran en ordre cronològic en la teva línia de temps Inici. No tinguis por en cometre errors, pots fàcilment deixar de seguir-los en qualsevol moment!",
+  "follow_recommendations.lead": "Les publicacions dels usuaris que segueixes es mostraran en ordre cronològic en la teva línia de temps Inici. No tinguis por en cometre errors, pots fàcilment deixar de seguir-los en qualsevol moment!",
   "follow_request.authorize": "Autoritza",
   "follow_request.reject": "Rebutja",
   "follow_requests.unlocked_explanation": "Tot i que el teu compte no està bloquejat, el personal de {domain} ha pensat que és possible que vulguis revisar les sol·licituds de seguiment d’aquests comptes manualment.",
@@ -294,7 +294,7 @@
   "interaction_modal.on_this_server": "En aquest servidor",
   "interaction_modal.other_server_instructions": "Copia i enganxa aquest enllaç en el camp de cerca de la teva aplicació Mastodon preferida o en l'interfície web del teu servidor Mastodon.",
   "interaction_modal.preamble": "Donat que Mastodon és descentralitzat, pots fer servir el teu compte existent a un altre servidor Mastodon o plataforma compatible si és que no tens compte en aquest.",
-  "interaction_modal.title.favourite": "Afavoreix la publicació de {name}",
+  "interaction_modal.title.favourite": "Marca la publicació de {name}",
   "interaction_modal.title.follow": "Segueix {name}",
   "interaction_modal.title.reblog": "Impulsa la publicació de {name}",
   "interaction_modal.title.reply": "Respon a la publicació de {name}",
@@ -310,7 +310,7 @@
   "keyboard_shortcuts.direct": "per obrir la columna de missatges directes",
   "keyboard_shortcuts.down": "Mou-lo avall en la llista",
   "keyboard_shortcuts.enter": "Obrir publicació",
-  "keyboard_shortcuts.favourite": "Afavoreix la publicació",
+  "keyboard_shortcuts.favourite": "Marca la publicació",
   "keyboard_shortcuts.favourites": "Obre la llista de preferits",
   "keyboard_shortcuts.federated": "Obre la línia de temps federada",
   "keyboard_shortcuts.heading": "Dreceres de teclat",
@@ -542,7 +542,7 @@
   "status.admin_account": "Obre l'interfície de moderació per a @{name}",
   "status.admin_status": "Obrir aquesta publicació a la interfície de moderació",
   "status.block": "Bloqueja @{name}",
-  "status.bookmark": "Afavoreix",
+  "status.bookmark": "Marca",
   "status.cancel_reblog_private": "Desfés l'impuls",
   "status.cannot_reblog": "Aquesta publicació no es pot impulsar",
   "status.copy": "Copia l'enllaç a la publicació",
diff --git a/app/javascript/mastodon/locales/cy.json b/app/javascript/mastodon/locales/cy.json
index 0d1149ef1..9812eec62 100644
--- a/app/javascript/mastodon/locales/cy.json
+++ b/app/javascript/mastodon/locales/cy.json
@@ -2,7 +2,7 @@
   "about.blocks": "Gweinyddion sy'n cael eu cymedroli",
   "about.contact": "Cyswllt:",
   "about.disclaimer": "Mae Mastodon yn feddalwedd rhydd, cod agored ac o dan hawlfraint Mastodon gGmbH.",
-  "about.domain_blocks.no_reason_available": "Reason not available",
+  "about.domain_blocks.no_reason_available": "Rheswm ddim ar gael",
   "about.domain_blocks.preamble": "Yn gyffredinol, mae Mastodon yn caniatáu i chi weld cynnwys gan unrhyw weinyddwr arall yn y ffederasiwn a rhyngweithio â hi. Dyma'r eithriadau a wnaed ar y gweinydd penodol hwn.",
   "about.domain_blocks.silenced.explanation": "Yn gyffredinol, fyddwch chi ddim yn gweld proffiliau a chynnwys o'r gweinydd hwn, oni bai eich bod yn chwilio'n benodol amdano neu yn ymuno drwy ei ddilyn.",
   "about.domain_blocks.silenced.title": "Tawelwyd",
@@ -28,7 +28,7 @@
   "account.endorse": "Arddangos ar fy mhroffil",
   "account.featured_tags.last_status_at": "Y cofnod diwethaf ar {date}",
   "account.featured_tags.last_status_never": "Dim postiadau",
-  "account.featured_tags.title": "{name}'s featured hashtags",
+  "account.featured_tags.title": "hashnodau dan sylw {name}",
   "account.follow": "Dilyn",
   "account.followers": "Dilynwyr",
   "account.followers.empty": "Does neb yn dilyn y defnyddiwr hwn eto.",
@@ -49,7 +49,7 @@
   "account.mute": "Tawelu @{name}",
   "account.mute_notifications": "Cuddio hysbysiadau o @{name}",
   "account.muted": "Distewyd",
-  "account.open_original_page": "Open original page",
+  "account.open_original_page": "Agor y dudalen wreiddiol",
   "account.posts": "Postiadau",
   "account.posts_with_replies": "Postiadau ac atebion",
   "account.report": "Adrodd @{name}",
@@ -95,7 +95,7 @@
   "closed_registrations.other_server_instructions": "Gan fod Mastodon yn ddatganoledig, gallwch greu cyfrif ar weinydd arall a dal i ryngweithio gyda hwn.",
   "closed_registrations_modal.description": "Ar hyn o bryd nid yw'n bosib creu cyfrif ar {domain}, ond cadwch mewn cof nad oes raid i chi gael cyfrif yn benodol ar {domain} i ddefnyddio Mastodon.",
   "closed_registrations_modal.find_another_server": "Dod o hyd i weinydd arall",
-  "closed_registrations_modal.preamble": "Mastodon is decentralized, so no matter where you create your account, you will be able to follow and interact with anyone on this server. You can even self-host it!",
+  "closed_registrations_modal.preamble": "Mae Mastodon wedi'i ddatganoli, felly does dim gwahaniaeth ble rydych chi'n creu eich cyfrif, byddwch chi'n gallu dilyn a rhyngweithio ag unrhyw un ar y gweinydd hwn. Gallwch hyd yn oed ei gynnal ef eich hun!",
   "closed_registrations_modal.title": "Cofrestru ar Mastodon",
   "column.about": "Ynghylch",
   "column.blocks": "Defnyddwyr a flociwyd",
@@ -181,12 +181,12 @@
   "directory.local": "O {domain} yn unig",
   "directory.new_arrivals": "Newydd-ddyfodiaid",
   "directory.recently_active": "Yn weithredol yn ddiweddar",
-  "disabled_account_banner.account_settings": "Account settings",
+  "disabled_account_banner.account_settings": "Gosodiadau'r cyfrif",
   "disabled_account_banner.text": "Mae eich cyfrif {disabledAccount} wedi ei analluogi ar hyn o bryd.",
   "dismissable_banner.community_timeline": "Dyma'r postiadau cyhoeddus diweddaraf gan bobl y caiff eu cyfrifon eu cynnal ar {domain}.",
   "dismissable_banner.dismiss": "Diystyru",
-  "dismissable_banner.explore_links": "These news stories are being talked about by people on this and other servers of the decentralized network right now.",
-  "dismissable_banner.explore_statuses": "These posts from this and other servers in the decentralized network are gaining traction on this server right now.",
+  "dismissable_banner.explore_links": "Mae'r straeon newyddion hyn yn cael eu trafod gan bobl ar y gweinydd hwn a rhai eraill ar y rhwydwaith datganoledig hwn, ar hyn o bryd.",
+  "dismissable_banner.explore_statuses": "Mae'r cofnodion hyn o'r gweinydd hwn a gweinyddion eraill yn y rhwydwaith datganoledig hwn yn denu sylw ar y gweinydd hwn ar hyn o bryd.",
   "dismissable_banner.explore_tags": "These hashtags are gaining traction among people on this and other servers of the decentralized network right now.",
   "dismissable_banner.public_timeline": "These are the most recent public posts from people on this and other servers of the decentralized network that this server knows about.",
   "embed.instructions": "Gosodwch y post hwn ar eich gwefan drwy gopïo'r côd isod.",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index 77aef2197..3cf08a855 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -42,7 +42,7 @@
   "account.joined_short": "Beigetreten",
   "account.languages": "Genutzte Sprachen überarbeiten",
   "account.link_verified_on": "Das Profil mit dieser E-Mail-Adresse wurde bereits am {date} bestätigt",
-  "account.locked_info": "Der Privatsphärenstatus dieses Kontos wurde auf „gesperrt“ gesetzt. Die Person bestimmt manuell, wer ihm/ihr folgen darf.",
+  "account.locked_info": "Die Privatsphäre dieses Kontos wurde auf „geschützt“ gesetzt. Die Person bestimmt manuell, wer ihrem Profil folgen darf.",
   "account.media": "Medien",
   "account.mention": "@{name} im Beitrag erwähnen",
   "account.moved_to": "{name} hat angegeben, dass dieser der neue Account ist:",
@@ -87,15 +87,15 @@
   "bundle_column_error.network.title": "Netzwerkfehler",
   "bundle_column_error.retry": "Erneut versuchen",
   "bundle_column_error.return": "Zurück zur Startseite",
-  "bundle_column_error.routing.body": "Die angeforderte Seite konnte nicht gefunden werden. Sind Sie sicher, dass die URL in der Adressleiste korrekt ist?",
+  "bundle_column_error.routing.body": "Die angeforderte Seite konnte nicht gefunden werden. Bist du dir sicher, dass die URL in der Adressleiste korrekt ist?",
   "bundle_column_error.routing.title": "404",
   "bundle_modal_error.close": "Schließen",
   "bundle_modal_error.message": "Etwas ist beim Laden schiefgelaufen.",
   "bundle_modal_error.retry": "Erneut versuchen",
-  "closed_registrations.other_server_instructions": "Da Mastodon dezentralisiert ist, können Sie ein Konto auf einem anderen Server erstellen und trotzdem mit diesem Server interagieren.",
-  "closed_registrations_modal.description": "Das Anlegen eines Kontos auf {domain} ist derzeit nicht möglich, aber bedenken Sie bitte, dass Sie kein spezielles Konto auf {domain} benötigen, um Mastodon nutzen zu können.",
+  "closed_registrations.other_server_instructions": "Da Mastodon dezentralisiert ist, kannst du ein Konto auf einem anderen Server erstellen und trotzdem mit diesem Server interagieren.",
+  "closed_registrations_modal.description": "Das Anlegen eines Kontos auf {domain} ist derzeit nicht möglich, aber bedenke, dass du kein extra Konto auf {domain} benötigst, um Mastodon nutzen zu können.",
   "closed_registrations_modal.find_another_server": "Einen anderen Server auswählen",
-  "closed_registrations_modal.preamble": "Mastodon ist dezentralisiert, d.h. unabhängig davon, wo Sie Ihr Konto erstellen, können Sie jedem auf diesem Server folgen und mit ihm interagieren. Sie können ihn sogar selbst hosten!",
+  "closed_registrations_modal.preamble": "Mastodon ist dezentralisiert, das heißt unabhängig davon, wo du dein Konto erstellst, kannst du jedes Konto auf diesem Server folgen und mit dem interagieren. Du kannst auch deinen eigenen Server hosten!",
   "closed_registrations_modal.title": "Bei Mastodon registrieren",
   "column.about": "Über",
   "column.blocks": "Blockierte Profile",
@@ -292,7 +292,7 @@
   "interaction_modal.description.reply": "Mit einem Account auf Mastodon kannst du auf diesen Beitrag antworten.",
   "interaction_modal.on_another_server": "Auf einem anderen Server",
   "interaction_modal.on_this_server": "Auf diesem Server",
-  "interaction_modal.other_server_instructions": "Kopieren Sie diese Adresse und fügen Sie diese in das Suchfeld Ihrer bevorzugten Mastodon-App oder in die Weboberfläche Ihres Mastodon-Servers ein.",
+  "interaction_modal.other_server_instructions": "Kopiere diese URL und füge sie in das Suchfeld deiner bevorzugten Mastodon-App oder im Webinterface deiner Mastodon-Instanz ein.",
   "interaction_modal.preamble": "Da Mastodon dezentralisiert ist, kannst du dein bestehendes Konto auf einem anderen Mastodon-Server oder einer kompatiblen Plattform nutzen, wenn du kein Konto auf dieser Plattform hast.",
   "interaction_modal.title.favourite": "Lieblingsbeitrag von {name}",
   "interaction_modal.title.follow": "Folge {name}",
@@ -341,7 +341,7 @@
   "lightbox.next": "Weiter",
   "lightbox.previous": "Zurück",
   "limited_account_hint.action": "Profil trotzdem anzeigen",
-  "limited_account_hint.title": "Dieses Profil wurde von den Moderator*innnen der Mastodon-Instanz {domain} ausgeblendet.",
+  "limited_account_hint.title": "Dieses Profil wurde von den Moderator*innen der Mastodon-Instanz {domain} ausgeblendet.",
   "lists.account.add": "Zur Liste hinzufügen",
   "lists.account.remove": "Von der Liste entfernen",
   "lists.delete": "Liste löschen",
@@ -491,7 +491,7 @@
   "report.placeholder": "Zusätzliche Kommentare",
   "report.reasons.dislike": "Das gefällt mir nicht",
   "report.reasons.dislike_description": "Es ist etwas, das du nicht sehen willst",
-  "report.reasons.other": "Da ist was anderes",
+  "report.reasons.other": "Es geht um etwas anderes",
   "report.reasons.other_description": "Das Problem passt nicht in die Kategorien",
   "report.reasons.spam": "Das ist Spam",
   "report.reasons.spam_description": "Bösartige Links, gefälschtes Engagement oder wiederholte Antworten",
@@ -545,7 +545,7 @@
   "status.bookmark": "Lesezeichen setzen",
   "status.cancel_reblog_private": "Teilen des Beitrags rückgängig machen",
   "status.cannot_reblog": "Dieser Beitrag kann nicht geteilt werden",
-  "status.copy": "Kopiere Link des Beitrags",
+  "status.copy": "Link zum Beitrag kopieren",
   "status.delete": "Beitrag löschen",
   "status.detailed_status": "Detaillierte Ansicht der Unterhaltung",
   "status.direct": "Direktnachricht an @{name}",
@@ -588,7 +588,7 @@
   "status.show_more_all": "Alle Inhaltswarnungen aufklappen",
   "status.show_original": "Original anzeigen",
   "status.translate": "Übersetzen",
-  "status.translated_from_with": "Von {lang} mit {provider} übersetzt",
+  "status.translated_from_with": "Aus {lang} mittels {provider} übersetzt",
   "status.uncached_media_warning": "Nicht verfügbar",
   "status.unmute_conversation": "Stummschaltung der Unterhaltung aufheben",
   "status.unpin": "Vom Profil lösen",
@@ -638,7 +638,7 @@
   "upload_modal.preparing_ocr": "Vorbereitung von OCR…",
   "upload_modal.preview_label": "Vorschau ({ratio})",
   "upload_progress.label": "Wird hochgeladen …",
-  "upload_progress.processing": "Wird verarbeitet …",
+  "upload_progress.processing": "Wird verarbeitet…",
   "video.close": "Video schließen",
   "video.download": "Datei herunterladen",
   "video.exit_fullscreen": "Vollbild verlassen",
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index 01a2821bc..12823083d 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -2,8 +2,8 @@
   "about.blocks": "Moderigitaj serviloj",
   "about.contact": "Kontakto:",
   "about.disclaimer": "Mastodon estas libera, malfermitkoda programaro kaj varmarko de la firmao Mastodon gGmbH.",
-  "about.domain_blocks.no_reason_available": "Kialo ne estas disponebla",
-  "about.domain_blocks.preamble": "Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.",
+  "about.domain_blocks.no_reason_available": "Kialo ne disponebla",
+  "about.domain_blocks.preamble": "Mastodono ebligas vidi enhavojn el uzantoj kaj komuniki kun ilin el aliaj serviloj el la Fediverso. Estas la limigoj deciditaj por tiu ĉi servilo.",
   "about.domain_blocks.silenced.explanation": "Vi ne ĝenerale vidos profilojn kaj enhavojn de ĉi tiu servilo, krom se vi eksplice trovas aŭ estas permesita de via sekvato.",
   "about.domain_blocks.silenced.title": "Limigita",
   "about.domain_blocks.suspended.explanation": "No data from this server will be processed, stored or exchanged, making any interaction or communication with users from this server impossible.",
@@ -40,7 +40,7 @@
   "account.go_to_profile": "Iri al profilo",
   "account.hide_reblogs": "Kaŝi la plusendojn de @{name}",
   "account.joined_short": "Aliĝis",
-  "account.languages": "Change subscribed languages",
+  "account.languages": "Ŝanĝi elekton de abonitaj lingvoj",
   "account.link_verified_on": "La posedanto de tiu ligilo estis kontrolita je {date}",
   "account.locked_info": "La privateco de tiu konto estas elektita kiel fermita. La posedanto povas mane akcepti tiun, kiu povas sekvi rin.",
   "account.media": "Aŭdovidaĵoj",
@@ -49,7 +49,7 @@
   "account.mute": "Silentigi @{name}",
   "account.mute_notifications": "Silentigi la sciigojn de @{name}",
   "account.muted": "Silentigita",
-  "account.open_original_page": "Open original page",
+  "account.open_original_page": "Malfermi originan paĝon",
   "account.posts": "Mesaĝoj",
   "account.posts_with_replies": "Mesaĝoj kaj respondoj",
   "account.report": "Raporti @{name}",
@@ -96,7 +96,7 @@
   "closed_registrations_modal.description": "Creating an account on {domain} is currently not possible, but please keep in mind that you do not need an account specifically on {domain} to use Mastodon.",
   "closed_registrations_modal.find_another_server": "Trovi alian servilon",
   "closed_registrations_modal.preamble": "Mastodon is decentralized, so no matter where you create your account, you will be able to follow and interact with anyone on this server. You can even self-host it!",
-  "closed_registrations_modal.title": "Registri en Mastodon",
+  "closed_registrations_modal.title": "Krei konton en Mastodon",
   "column.about": "Pri",
   "column.blocks": "Blokitaj uzantoj",
   "column.bookmarks": "Legosignoj",
@@ -389,7 +389,7 @@
   "navigation_bar.security": "Sekureco",
   "not_signed_in_indicator.not_signed_in": "You need to sign in to access this resource.",
   "notification.admin.report": "{name} raportis {target}",
-  "notification.admin.sign_up": "{name} registris",
+  "notification.admin.sign_up": "{name} kreis konton",
   "notification.favourite": "{name} aldonis vian mesaĝon al siaj preferaĵoj",
   "notification.follow": "{name} eksekvis vin",
   "notification.follow_request": "{name} petis sekvi vin",
@@ -464,8 +464,8 @@
   "relative_time.full.days": "antaŭ {number, plural, one {# tago} other {# tagoj}}",
   "relative_time.full.hours": "antaŭ {number, plural, one {# horo} other {# horoj}}",
   "relative_time.full.just_now": "ĵus nun",
-  "relative_time.full.minutes": "{number, plural, one {# minute} other {# minutes}} ago",
-  "relative_time.full.seconds": "{number, plural, one {# second} other {# seconds}} ago",
+  "relative_time.full.minutes": "antaŭ {number, plural, one {# minuto} other {# minutoj}}",
+  "relative_time.full.seconds": "antaŭ {number, plural, one {# sekundo} other {# sekundoj}}",
   "relative_time.hours": "{number}h",
   "relative_time.just_now": "nun",
   "relative_time.minutes": "{number}m",
@@ -476,7 +476,7 @@
   "report.block_explanation": "You will not see their posts. They will not be able to see your posts or follow you. They will be able to tell that they are blocked.",
   "report.categories.other": "Aliaj",
   "report.categories.spam": "Trudmesaĝo",
-  "report.categories.violation": "Content violates one or more server rules",
+  "report.categories.violation": "Enhavo malobservas unu aŭ plurajn servilajn regulojn",
   "report.category.subtitle": "Elektu la plej bonan kongruon",
   "report.category.title": "Diru al ni kio okazas pri ĉi tiu {type}",
   "report.category.title_account": "profilo",
@@ -528,15 +528,15 @@
   "search_results.nothing_found": "Povis trovi nenion por ĉi tiuj serĉaj terminoj",
   "search_results.statuses": "Mesaĝoj",
   "search_results.statuses_fts_disabled": "Serĉi mesaĝojn laŭ enhavo ne estas ebligita en ĉi tiu Mastodon-servilo.",
-  "search_results.title": "Search for {q}",
+  "search_results.title": "Serĉ-rezultoj por {q}",
   "search_results.total": "{count, number} {count, plural, one {rezulto} other {rezultoj}}",
   "server_banner.about_active_users": "People using this server during the last 30 days (Monthly Active Users)",
   "server_banner.active_users": "active users",
-  "server_banner.administered_by": "Administered by:",
+  "server_banner.administered_by": "Administrata de:",
   "server_banner.introduction": "{domain} is part of the decentralized social network powered by {mastodon}.",
   "server_banner.learn_more": "Learn more",
-  "server_banner.server_stats": "Server stats:",
-  "sign_in_banner.create_account": "Create account",
+  "server_banner.server_stats": "Statistikoj de la servilo:",
+  "sign_in_banner.create_account": "Krei konton",
   "sign_in_banner.sign_in": "Sign in",
   "sign_in_banner.text": "Sign in to follow profiles or hashtags, favourite, share and reply to posts, or interact from your account on a different server.",
   "status.admin_account": "Malfermi la kontrolan interfacon por @{name}",
@@ -587,13 +587,13 @@
   "status.show_more": "Montri pli",
   "status.show_more_all": "Montri pli ĉiun",
   "status.show_original": "Show original",
-  "status.translate": "Translate",
-  "status.translated_from_with": "Translated from {lang} using {provider}",
+  "status.translate": "Traduki",
+  "status.translated_from_with": "Tradukita el {lang} per {provider}",
   "status.uncached_media_warning": "Nedisponebla",
   "status.unmute_conversation": "Malsilentigi la konversacion",
   "status.unpin": "Depingli de profilo",
   "subscribed_languages.lead": "Only posts in selected languages will appear on your home and list timelines after the change. Select none to receive posts in all languages.",
-  "subscribed_languages.save": "Save changes",
+  "subscribed_languages.save": "Konservi ŝanĝojn",
   "subscribed_languages.target": "Change subscribed languages for {target}",
   "suggestions.dismiss": "Forigi la proponon",
   "suggestions.header": "Vi povus interesiĝi pri…",
@@ -610,7 +610,7 @@
   "timeline_hint.resources.followers": "Sekvantoj",
   "timeline_hint.resources.follows": "Sekvatoj",
   "timeline_hint.resources.statuses": "Pli malnovaj mesaĝoj",
-  "trends.counter_by_accounts": "{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {{days} days}}",
+  "trends.counter_by_accounts": "{count, plural, one {{counter} persono} other {{counter} personoj}} dum la pasinta{days, plural, one { tago} other {j {days} tagoj}}",
   "trends.trending_now": "Nunaj furoraĵoj",
   "ui.beforeunload": "Via malneto perdiĝos se vi eliras de Mastodon.",
   "units.short.billion": "{count}Md",
diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json
index deb539d0b..74a6acd26 100644
--- a/app/javascript/mastodon/locales/es-AR.json
+++ b/app/javascript/mastodon/locales/es-AR.json
@@ -2,7 +2,7 @@
   "about.blocks": "Servidores moderados",
   "about.contact": "Contacto:",
   "about.disclaimer": "Mastodon es software libre y de código abierto y una marca comercial de Mastodon gGmbH.",
-  "about.domain_blocks.no_reason_available": "Reason not available",
+  "about.domain_blocks.no_reason_available": "Motivo no disponible",
   "about.domain_blocks.preamble": "Mastodon normalmente te permite ver el contenido e interactuar con los usuarios de cualquier otro servidor en el fediverso. Estas son las excepciones que se han hecho en este servidor en particular.",
   "about.domain_blocks.silenced.explanation": "Normalmente no verás perfiles y contenido de este servidor, a menos que lo busqués explícitamente o sigás alguna cuenta.",
   "about.domain_blocks.silenced.title": "Limitados",
@@ -49,7 +49,7 @@
   "account.mute": "Silenciar a @{name}",
   "account.mute_notifications": "Silenciar notificaciones de @{name}",
   "account.muted": "Silenciado",
-  "account.open_original_page": "Open original page",
+  "account.open_original_page": "Abrir página original",
   "account.posts": "Mensajes",
   "account.posts_with_replies": "Mnsjs y resp. públicas",
   "account.report": "Denunciar a @{name}",
@@ -292,7 +292,7 @@
   "interaction_modal.description.reply": "Con una cuenta en Mastodon, podés responder a este mensaje.",
   "interaction_modal.on_another_server": "En un servidor diferente",
   "interaction_modal.on_this_server": "En este servidor",
-  "interaction_modal.other_server_instructions": "Copia y pega esta URL en la barra de búsqueda de tu aplicación Mastodon favorita o la interfaz web de tu servidor Mastodon.",
+  "interaction_modal.other_server_instructions": "Copiá y pegá esta dirección web en la barra de búsqueda de tu aplicación favorita de Mastodon, o en la interface web de tu servidor de Mastodon.",
   "interaction_modal.preamble": "Ya que Mastodon es descentralizado, podés usar tu cuenta existente alojada por otro servidor Mastodon (u otra plataforma compatible, si no tenés una cuenta en ésta).",
   "interaction_modal.title.favourite": "Marcar como favorito el mensaje de {name}",
   "interaction_modal.title.follow": "Seguir a {name}",
@@ -614,8 +614,8 @@
   "trends.trending_now": "Tendencia ahora",
   "ui.beforeunload": "Tu borrador se perderá si abandonás Mastodon.",
   "units.short.billion": "{count}MM",
-  "units.short.million": "{count}M",
-  "units.short.thousand": "{count}mil",
+  "units.short.million": "{count} M",
+  "units.short.thousand": "{count} mil",
   "upload_area.title": "Para subir, arrastrá y soltá",
   "upload_button.label": "Agregá imágenes, o un archivo de audio o video",
   "upload_error.limit": "Se excedió el límite de subida de archivos.",
diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json
index bdbdb8565..c4c625d2f 100644
--- a/app/javascript/mastodon/locales/es.json
+++ b/app/javascript/mastodon/locales/es.json
@@ -501,7 +501,7 @@
   "report.rules.title": "¿Qué normas se están violando?",
   "report.statuses.subtitle": "Selecciona todos los que correspondan",
   "report.statuses.title": "¿Hay alguna publicación que respalde este informe?",
-  "report.submit": "Publicar",
+  "report.submit": "Enviar",
   "report.target": "Reportando",
   "report.thanks.take_action": "Aquí están tus opciones para controlar lo que ves en Mastodon:",
   "report.thanks.take_action_actionable": "Mientras revisamos esto, puedes tomar medidas contra @{name}:",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index eec5416c5..3d1454866 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -2,7 +2,7 @@
   "about.blocks": "Moderoidut palvelimet",
   "about.contact": "Yhteystiedot:",
   "about.disclaimer": "Mastodon on vapaa avoimen lähdekoodin ohjelmisto ja Mastodon gGmbH:n tavaramerkki.",
-  "about.domain_blocks.no_reason_available": "Reason not available",
+  "about.domain_blocks.no_reason_available": "Syy ei saatavilla",
   "about.domain_blocks.preamble": "Mastodonin avulla voit yleensä tarkastella sisältöä ja olla vuorovaikutuksessa käyttäjien kanssa millä tahansa muulla palvelimella fediversessä. Nämä ovat poikkeuksia, jotka on tehty tälle palvelimelle.",
   "about.domain_blocks.silenced.explanation": "Et yleensä näe profiileja ja sisältöä tältä palvelimelta, ellet nimenomaisesti etsi tai valitse sitä seuraamalla.",
   "about.domain_blocks.silenced.title": "Rajoitettu",
@@ -49,7 +49,7 @@
   "account.mute": "Mykistä @{name}",
   "account.mute_notifications": "Mykistä ilmoitukset käyttäjältä @{name}",
   "account.muted": "Mykistetty",
-  "account.open_original_page": "Open original page",
+  "account.open_original_page": "Avaa alkuperäinen sivu",
   "account.posts": "Viestit",
   "account.posts_with_replies": "Viestit ja vastaukset",
   "account.report": "Raportoi @{name}",
diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json
index 3410a8b7d..62a71b122 100644
--- a/app/javascript/mastodon/locales/gl.json
+++ b/app/javascript/mastodon/locales/gl.json
@@ -2,7 +2,7 @@
   "about.blocks": "Servidores moderados",
   "about.contact": "Contacto:",
   "about.disclaimer": "Mastodon é software libre, de código aberto, e unha marca comercial de Mastodon gGmbH.",
-  "about.domain_blocks.no_reason_available": "Reason not available",
+  "about.domain_blocks.no_reason_available": "Non está indicada a razón",
   "about.domain_blocks.preamble": "Mastodon de xeito xeral permíteche ver contidos doutros servidores do fediverso e interactuar coas súas usuarias. Estas son as excepcións que se estabeleceron neste servidor en particular.",
   "about.domain_blocks.silenced.explanation": "Por defecto non verás perfís e contido desde este servidor, a menos que mires de xeito explícito ou optes por seguir ese contido ou usuaria.",
   "about.domain_blocks.silenced.title": "Limitado",
@@ -49,7 +49,7 @@
   "account.mute": "Acalar @{name}",
   "account.mute_notifications": "Acalar as notificacións de @{name}",
   "account.muted": "Acalada",
-  "account.open_original_page": "Open original page",
+  "account.open_original_page": "Abrir páxina orixinal",
   "account.posts": "Publicacións",
   "account.posts_with_replies": "Publicacións e respostas",
   "account.report": "Informar sobre @{name}",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index 78d5d9e0d..9bc323607 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -2,7 +2,7 @@
   "about.blocks": "制限中のサーバー",
   "about.contact": "連絡先",
   "about.disclaimer": "Mastodonは自由なオープンソースソフトウェアでMastodon gGmbHの商標です。",
-  "about.domain_blocks.no_reason_available": "Reason not available",
+  "about.domain_blocks.no_reason_available": "制限理由",
   "about.domain_blocks.preamble": "Mastodonでは連合先のどのようなサーバーのユーザーとも交流できます。ただし次のサーバーには例外が設定されています。",
   "about.domain_blocks.silenced.explanation": "このサーバーのプロフィールやコンテンツは、明示的に検索したり、フォローでオプトインしない限り、通常は表示されません。",
   "about.domain_blocks.silenced.title": "制限",
@@ -49,7 +49,7 @@
   "account.mute": "@{name}さんをミュート",
   "account.mute_notifications": "@{name}さんからの通知を受け取らない",
   "account.muted": "ミュート済み",
-  "account.open_original_page": "Open original page",
+  "account.open_original_page": "元のページを開く",
   "account.posts": "投稿",
   "account.posts_with_replies": "投稿と返信",
   "account.report": "@{name}さんを通報",
@@ -296,7 +296,7 @@
   "interaction_modal.description.reply": "Mastodonのアカウントでこの投稿に反応できます。",
   "interaction_modal.on_another_server": "別のサーバー",
   "interaction_modal.on_this_server": "このサーバー",
-  "interaction_modal.other_server_instructions": "Copy and paste this URL into the search field of your favourite Mastodon app or the web interface of your Mastodon server.",
+  "interaction_modal.other_server_instructions": "このURLをお気に入りのMastodonアプリやMastodonサーバーのWebインターフェースの検索フィールドにコピーして貼り付けます。",
   "interaction_modal.preamble": "Mastodonは分散化されているためアカウントを持っていなくても別のMastodonサーバーまたは互換性のあるプラットフォームでホストされているアカウントを使用できます。",
   "interaction_modal.title.favourite": "{name}さんの投稿をお気に入り",
   "interaction_modal.title.follow": "{name}さんをフォロー",
diff --git a/app/javascript/mastodon/locales/lv.json b/app/javascript/mastodon/locales/lv.json
index 89c3aee4e..711a003fe 100644
--- a/app/javascript/mastodon/locales/lv.json
+++ b/app/javascript/mastodon/locales/lv.json
@@ -461,11 +461,11 @@
   "regeneration_indicator.label": "Ielādē…",
   "regeneration_indicator.sublabel": "Tiek gatavota tava plūsma!",
   "relative_time.days": "{number}d",
-  "relative_time.full.days": "{number, plural, one {# diena} other {# dienas}} atpakaļ",
-  "relative_time.full.hours": "{number, plural, one {# stunda} other {# stundas}} atpakaļ",
+  "relative_time.full.days": "Pirms {number, plural, one {# dienas} other {# dienām}}",
+  "relative_time.full.hours": "Pirms {number, plural, one {# stundas} other {# stundām}}",
   "relative_time.full.just_now": "tikko",
-  "relative_time.full.minutes": "{number, plural, one {# minūte} other {# minūtes}} atpakaļ",
-  "relative_time.full.seconds": "{number, plural, one {# sekunde} other {# sekundes}} atpakaļ",
+  "relative_time.full.minutes": "Pirms {number, plural, one {# minūtes} other {# minūtēm}}",
+  "relative_time.full.seconds": "Pirms {number, plural, one {# sekundes} other {# sekundēm}}",
   "relative_time.hours": "{number}st",
   "relative_time.just_now": "tagad",
   "relative_time.minutes": "{number}m",
@@ -553,7 +553,7 @@
   "status.edited": "Rediģēts {date}",
   "status.edited_x_times": "Rediģēts {count, plural, one {{count} reize} other {{count} reizes}}",
   "status.embed": "Iestrādāt",
-  "status.favourite": "Iecienītā",
+  "status.favourite": "Patīk",
   "status.filter": "Filtrē šo ziņu",
   "status.filtered": "Filtrēts",
   "status.hide": "Slēpt",
diff --git a/app/javascript/mastodon/locales/ml.json b/app/javascript/mastodon/locales/ml.json
index 0517ce02a..51afa4b43 100644
--- a/app/javascript/mastodon/locales/ml.json
+++ b/app/javascript/mastodon/locales/ml.json
@@ -1,8 +1,8 @@
 {
-  "about.blocks": "Moderated servers",
-  "about.contact": "Contact:",
+  "about.blocks": "മോഡറേറ്റഡ് സെർവറുകൾ",
+  "about.contact": "ബന്ധപ്പെടുക:",
   "about.disclaimer": "Mastodon is free, open-source software, and a trademark of Mastodon gGmbH.",
-  "about.domain_blocks.no_reason_available": "Reason not available",
+  "about.domain_blocks.no_reason_available": "കാരണം ലഭ്യമല്",
   "about.domain_blocks.preamble": "Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.",
   "about.domain_blocks.silenced.explanation": "You will generally not see profiles and content from this server, unless you explicitly look it up or opt into it by following.",
   "about.domain_blocks.silenced.title": "Limited",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index f3742b5a2..131d56298 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -2,7 +2,7 @@
   "about.blocks": "Gemodereerde servers",
   "about.contact": "Contact:",
   "about.disclaimer": "Mastodon is vrije, opensourcesoftware en een handelsmerk van Mastodon gGmbH.",
-  "about.domain_blocks.no_reason_available": "Reason not available",
+  "about.domain_blocks.no_reason_available": "Reden niet beschikbaar",
   "about.domain_blocks.preamble": "In het algemeen kun je met Mastodon berichten ontvangen van, en interactie hebben met gebruikers van elke server in de fediverse. Dit zijn de uitzonderingen die op deze specifieke server gelden.",
   "about.domain_blocks.silenced.explanation": "In het algemeen zie je geen berichten en accounts van deze server, tenzij je berichten expliciet opzoekt of ervoor kiest om een account van deze server te volgen.",
   "about.domain_blocks.silenced.title": "Beperkt",
@@ -49,7 +49,7 @@
   "account.mute": "@{name} negeren",
   "account.mute_notifications": "Meldingen van @{name} negeren",
   "account.muted": "Genegeerd",
-  "account.open_original_page": "Open original page",
+  "account.open_original_page": "Originele pagina openen",
   "account.posts": "Berichten",
   "account.posts_with_replies": "Berichten en reacties",
   "account.report": "@{name} rapporteren",
diff --git a/app/javascript/mastodon/locales/nn.json b/app/javascript/mastodon/locales/nn.json
index 83e0ec9c3..6113e32d0 100644
--- a/app/javascript/mastodon/locales/nn.json
+++ b/app/javascript/mastodon/locales/nn.json
@@ -151,7 +151,7 @@
   "confirmations.block.confirm": "Blokker",
   "confirmations.block.message": "Er du sikker på at du vil blokkera {name}?",
   "confirmations.cancel_follow_request.confirm": "Trekk attende førespurnad",
-  "confirmations.cancel_follow_request.message": "Er du sikker på at du vil trekke attende førespurnaden din for å fylgje {name}?",
+  "confirmations.cancel_follow_request.message": "Er du sikker på at du vil trekkje attende førespurnaden din om å fylgje {name}?",
   "confirmations.delete.confirm": "Slett",
   "confirmations.delete.message": "Er du sikker på at du vil sletta denne statusen?",
   "confirmations.delete_list.confirm": "Slett",
diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json
index ff8be31ee..6ef9fd040 100644
--- a/app/javascript/mastodon/locales/no.json
+++ b/app/javascript/mastodon/locales/no.json
@@ -110,7 +110,7 @@
   "column.lists": "Lister",
   "column.mutes": "Dempede brukere",
   "column.notifications": "Varsler",
-  "column.pins": "Pinned toot",
+  "column.pins": "Festede innlegg",
   "column.public": "Felles tidslinje",
   "column_back_button.label": "Tilbake",
   "column_header.hide_settings": "Skjul innstillinger",
@@ -265,7 +265,7 @@
   "footer.directory": "Profilkatalog",
   "footer.get_app": "Last ned appen",
   "footer.invite": "Invitér folk",
-  "footer.keyboard_shortcuts": "Keyboard shortcuts",
+  "footer.keyboard_shortcuts": "Hurtigtaster",
   "footer.privacy_policy": "Personvernregler",
   "footer.source_code": "Vis kildekode",
   "generic.saved": "Lagret",
@@ -494,26 +494,26 @@
   "report.reasons.other": "Det er noe annet",
   "report.reasons.other_description": "Problemet passer ikke inn i de andre kategoriene",
   "report.reasons.spam": "Det er spam",
-  "report.reasons.spam_description": "Malicious links, fake engagement, or repetitive replies",
+  "report.reasons.spam_description": "Ondsinnede lenker, falsk engasjement eller repeterende svar",
   "report.reasons.violation": "Det bryter serverregler",
   "report.reasons.violation_description": "Du er klar over at det bryter spesifikke regler",
   "report.rules.subtitle": "Velg alle som passer",
   "report.rules.title": "Hvilke regler brytes?",
   "report.statuses.subtitle": "Velg alle som passer",
-  "report.statuses.title": "Are there any posts that back up this report?",
+  "report.statuses.title": "Er det noen innlegg som støtter opp under denne rapporten?",
   "report.submit": "Send inn",
   "report.target": "Rapporterer",
   "report.thanks.take_action": "Her er alternativene dine for å kontrollere hva du ser på Mastodon:",
-  "report.thanks.take_action_actionable": "While we review this, you can take action against @{name}:",
+  "report.thanks.take_action_actionable": "Mens vi går gjennom dette, kan du iverksettet tiltak mot @{name}:",
   "report.thanks.title": "Ønsker du ikke å se dette?",
-  "report.thanks.title_actionable": "Thanks for reporting, we'll look into this.",
+  "report.thanks.title_actionable": "Takk for at du rapporterer, vi skal se på dette.",
   "report.unfollow": "Slutt å følge @{name}",
   "report.unfollow_explanation": "Du følger denne kontoen. For ikke å se innleggene deres i din hjem-feed lenger, slutt å følge dem.",
   "report_notification.attached_statuses": "{count, plural,one {{count} innlegg} other {{count} innlegg}} vedlagt",
   "report_notification.categories.other": "Annet",
   "report_notification.categories.spam": "Søppelpost",
   "report_notification.categories.violation": "Regelbrudd",
-  "report_notification.open": "Open report",
+  "report_notification.open": "Åpne rapport",
   "search.placeholder": "Søk",
   "search.search_or_paste": "Søk eller lim inn URL",
   "search_popout.search_format": "Avansert søkeformat",
@@ -592,9 +592,9 @@
   "status.uncached_media_warning": "Ikke tilgjengelig",
   "status.unmute_conversation": "Ikke demp samtale",
   "status.unpin": "Angre festing på profilen",
-  "subscribed_languages.lead": "Only posts in selected languages will appear on your home and list timelines after the change. Select none to receive posts in all languages.",
-  "subscribed_languages.save": "Save changes",
-  "subscribed_languages.target": "Change subscribed languages for {target}",
+  "subscribed_languages.lead": "Bare innlegg på valgte språk vil dukke opp i dine hjem- og liste-tidslinjer etter endringen. Velg ingen for å motta innlegg på alle språk.",
+  "subscribed_languages.save": "Lagre endringer",
+  "subscribed_languages.target": "Endre abbonerte språk for {target}",
   "suggestions.dismiss": "Utelukk forslaget",
   "suggestions.header": "Du er kanskje interessert i …",
   "tabs_bar.federated_timeline": "Felles",
@@ -610,7 +610,7 @@
   "timeline_hint.resources.followers": "Følgere",
   "timeline_hint.resources.follows": "Følger",
   "timeline_hint.resources.statuses": "Eldre innlegg",
-  "trends.counter_by_accounts": "{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {{days} days}}",
+  "trends.counter_by_accounts": "{count, plural, one {{counter} person} other {{counter} folk}} {days, plural, one {den siste dagen} other {de siste {days} dagene}}",
   "trends.trending_now": "Trender nå",
   "ui.beforeunload": "Din kladd vil bli forkastet om du forlater Mastodon.",
   "units.short.billion": "{count}m.ard",
@@ -629,7 +629,7 @@
   "upload_form.video_description": "Beskriv det for folk med hørselstap eller synshemminger",
   "upload_modal.analyzing_picture": "Analyserer bildet …",
   "upload_modal.apply": "Bruk",
-  "upload_modal.applying": "Applying…",
+  "upload_modal.applying": "Utfører…",
   "upload_modal.choose_image": "Velg et bilde",
   "upload_modal.description_placeholder": "Når du en gang kommer, neste sommer, skal vi atter drikke vin",
   "upload_modal.detect_text": "Oppdag tekst i bildet",
@@ -638,7 +638,7 @@
   "upload_modal.preparing_ocr": "Forbereder OCR…",
   "upload_modal.preview_label": "Forhåndsvisning ({ratio})",
   "upload_progress.label": "Laster opp...",
-  "upload_progress.processing": "Processing…",
+  "upload_progress.processing": "Behandler…",
   "video.close": "Lukk video",
   "video.download": "Last ned fil",
   "video.exit_fullscreen": "Lukk fullskjerm",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index 4728d7286..78b09ca53 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -9,7 +9,7 @@
   "about.domain_blocks.suspended.explanation": "Żadne dane z tego serwera nie będą przetwarzane, przechowywane lub wymieniane, co uniemożliwia jakąkolwiek interakcję lub komunikację z użytkownikami z tego serwera.",
   "about.domain_blocks.suspended.title": "Zawieszono",
   "about.not_available": "Ta informacja nie została udostępniona na tym serwerze.",
-  "about.powered_by": "Zdecentralizowane media społecznościowe w technologii {mastodon}",
+  "about.powered_by": "Zdecentralizowane media społecznościowe napędzane przez {mastodon}",
   "about.rules": "Regulamin serwera",
   "account.account_note_header": "Notatka",
   "account.add_or_remove_from_list": "Dodaj lub usuń z list",
@@ -336,7 +336,7 @@
   "keyboard_shortcuts.start": "aby otworzyć kolumnę „Rozpocznij”",
   "keyboard_shortcuts.toggle_hidden": "aby wyświetlić lub ukryć wpis spod CW",
   "keyboard_shortcuts.toggle_sensitivity": "by pokazać/ukryć multimedia",
-  "keyboard_shortcuts.toot": "aby utworzyć nowy wpis",
+  "keyboard_shortcuts.toot": "Stwórz nowy post",
   "keyboard_shortcuts.unfocus": "aby opuścić pole wyszukiwania/pisania",
   "keyboard_shortcuts.up": "aby przejść na górę listy",
   "lightbox.close": "Zamknij",
diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json
index bbc1aa0c3..d02fb0bec 100644
--- a/app/javascript/mastodon/locales/sk.json
+++ b/app/javascript/mastodon/locales/sk.json
@@ -1,16 +1,16 @@
 {
   "about.blocks": "Moderated servers",
-  "about.contact": "Contact:",
+  "about.contact": "Kontakt:",
   "about.disclaimer": "Mastodon is free, open-source software, and a trademark of Mastodon gGmbH.",
   "about.domain_blocks.no_reason_available": "Reason not available",
   "about.domain_blocks.preamble": "Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.",
   "about.domain_blocks.silenced.explanation": "You will generally not see profiles and content from this server, unless you explicitly look it up or opt into it by following.",
-  "about.domain_blocks.silenced.title": "Limited",
+  "about.domain_blocks.silenced.title": "Obmedzená",
   "about.domain_blocks.suspended.explanation": "No data from this server will be processed, stored or exchanged, making any interaction or communication with users from this server impossible.",
-  "about.domain_blocks.suspended.title": "Suspended",
+  "about.domain_blocks.suspended.title": "Vylúčený/á",
   "about.not_available": "This information has not been made available on this server.",
   "about.powered_by": "Decentralized social media powered by {mastodon}",
-  "about.rules": "Server rules",
+  "about.rules": "Serverové pravidlá",
   "account.account_note_header": "Poznámka",
   "account.add_or_remove_from_list": "Pridaj do, alebo odober zo zoznamov",
   "account.badges.bot": "Bot",
@@ -26,8 +26,8 @@
   "account.edit_profile": "Uprav profil",
   "account.enable_notifications": "Oboznamuj ma, keď má @{name} príspevky",
   "account.endorse": "Zobrazuj na profile",
-  "account.featured_tags.last_status_at": "Last post on {date}",
-  "account.featured_tags.last_status_never": "No posts",
+  "account.featured_tags.last_status_at": "Posledný príspevok dňa {date}",
+  "account.featured_tags.last_status_never": "Žiadne príspevky",
   "account.featured_tags.title": "{name}'s featured hashtags",
   "account.follow": "Nasleduj",
   "account.followers": "Sledujúci",
@@ -37,9 +37,9 @@
   "account.following_counter": "{count, plural, one {{counter} Following} other {{counter} Following}}",
   "account.follows.empty": "Tento používateľ ešte nikoho nenasleduje.",
   "account.follows_you": "Nasleduje ťa",
-  "account.go_to_profile": "Go to profile",
+  "account.go_to_profile": "Prejdi na profil",
   "account.hide_reblogs": "Skry vyzdvihnutia od @{name}",
-  "account.joined_short": "Joined",
+  "account.joined_short": "Pridal/a sa",
   "account.languages": "Change subscribed languages",
   "account.link_verified_on": "Vlastníctvo tohto odkazu bolo skontrolované {date}",
   "account.locked_info": "Stav súkromia pre tento účet je nastavený na zamknutý. Jeho vlastník sám prehodnocuje, kto ho môže sledovať.",
@@ -51,7 +51,7 @@
   "account.muted": "Nevšímaný/á",
   "account.open_original_page": "Open original page",
   "account.posts": "Príspevky/ov",
-  "account.posts_with_replies": "Príspevky, aj s odpoveďami",
+  "account.posts_with_replies": "Príspevky a odpovede",
   "account.report": "Nahlás @{name}",
   "account.requested": "Čaká na schválenie. Klikni pre zrušenie žiadosti",
   "account.share": "Zdieľaj @{name} profil",
@@ -84,20 +84,20 @@
   "bundle_column_error.error.body": "The requested page could not be rendered. It could be due to a bug in our code, or a browser compatibility issue.",
   "bundle_column_error.error.title": "Oh, no!",
   "bundle_column_error.network.body": "There was an error when trying to load this page. This could be due to a temporary problem with your internet connection or this server.",
-  "bundle_column_error.network.title": "Network error",
+  "bundle_column_error.network.title": "Chyba siete",
   "bundle_column_error.retry": "Skús to znova",
-  "bundle_column_error.return": "Go back home",
+  "bundle_column_error.return": "Prejdi späť na domovskú stránku",
   "bundle_column_error.routing.body": "The requested page could not be found. Are you sure the URL in the address bar is correct?",
   "bundle_column_error.routing.title": "404",
   "bundle_modal_error.close": "Zatvor",
   "bundle_modal_error.message": "Nastala chyba pri načítaní tohto komponentu.",
   "bundle_modal_error.retry": "Skúsiť znova",
   "closed_registrations.other_server_instructions": "Since Mastodon is decentralized, you can create an account on another server and still interact with this one.",
-  "closed_registrations_modal.description": "Creating an account on {domain} is currently not possible, but please keep in mind that you do not need an account specifically on {domain} to use Mastodon.",
-  "closed_registrations_modal.find_another_server": "Find another server",
+  "closed_registrations_modal.description": "Vytvorenie účtu na {domain} nie je v súčasnosti možné, ale majte prosím na pamäti, že nepotrebujete účet práve na {domain}, aby bolo možné používať Mastodon.",
+  "closed_registrations_modal.find_another_server": "Nájdi iný server",
   "closed_registrations_modal.preamble": "Mastodon is decentralized, so no matter where you create your account, you will be able to follow and interact with anyone on this server. You can even self-host it!",
-  "closed_registrations_modal.title": "Signing up on Mastodon",
-  "column.about": "About",
+  "closed_registrations_modal.title": "Registrácia na Mastodon",
+  "column.about": "O tomto serveri",
   "column.blocks": "Blokovaní užívatelia",
   "column.bookmarks": "Záložky",
   "column.community": "Miestna časová os",
@@ -175,13 +175,13 @@
   "conversation.mark_as_read": "Označ za prečítané",
   "conversation.open": "Ukáž konverzáciu",
   "conversation.with": "S {names}",
-  "copypaste.copied": "Copied",
-  "copypaste.copy": "Copy",
+  "copypaste.copied": "Skopírované",
+  "copypaste.copy": "Kopíruj",
   "directory.federated": "Zo známého fedivesmíru",
   "directory.local": "Iba z {domain}",
   "directory.new_arrivals": "Nové príchody",
   "directory.recently_active": "Nedávno aktívne",
-  "disabled_account_banner.account_settings": "Account settings",
+  "disabled_account_banner.account_settings": "Nastavenia účtu",
   "disabled_account_banner.text": "Your account {disabledAccount} is currently disabled.",
   "dismissable_banner.community_timeline": "These are the most recent public posts from people whose accounts are hosted by {domain}.",
   "dismissable_banner.dismiss": "Dismiss",
@@ -264,10 +264,10 @@
   "footer.about": "About",
   "footer.directory": "Profiles directory",
   "footer.get_app": "Get the app",
-  "footer.invite": "Invite people",
-  "footer.keyboard_shortcuts": "Keyboard shortcuts",
-  "footer.privacy_policy": "Privacy policy",
-  "footer.source_code": "View source code",
+  "footer.invite": "Pozvi ľudí",
+  "footer.keyboard_shortcuts": "Klávesové skratky",
+  "footer.privacy_policy": "Zásady súkromia",
+  "footer.source_code": "Zobraziť zdrojový kód",
   "generic.saved": "Uložené",
   "getting_started.heading": "Začni tu",
   "hashtag.column_header.tag_mode.all": "a {additional}",
@@ -290,14 +290,14 @@
   "interaction_modal.description.follow": "With an account on Mastodon, you can follow {name} to receive their posts in your home feed.",
   "interaction_modal.description.reblog": "With an account on Mastodon, you can boost this post to share it with your own followers.",
   "interaction_modal.description.reply": "With an account on Mastodon, you can respond to this post.",
-  "interaction_modal.on_another_server": "On a different server",
-  "interaction_modal.on_this_server": "On this server",
+  "interaction_modal.on_another_server": "Na inom serveri",
+  "interaction_modal.on_this_server": "Na tomto serveri",
   "interaction_modal.other_server_instructions": "Copy and paste this URL into the search field of your favourite Mastodon app or the web interface of your Mastodon server.",
   "interaction_modal.preamble": "Since Mastodon is decentralized, you can use your existing account hosted by another Mastodon server or compatible platform if you don't have an account on this one.",
   "interaction_modal.title.favourite": "Favourite {name}'s post",
-  "interaction_modal.title.follow": "Follow {name}",
-  "interaction_modal.title.reblog": "Boost {name}'s post",
-  "interaction_modal.title.reply": "Reply to {name}'s post",
+  "interaction_modal.title.follow": "Nasleduj {name}",
+  "interaction_modal.title.reblog": "Vyzdvihni {name}ov/in príspevok",
+  "interaction_modal.title.reply": "Odpovedz na {name}ov/in príspevok",
   "intervals.full.days": "{number, plural, one {# deň} few {# dní} many {# dní} other {# dní}}",
   "intervals.full.hours": "{number, plural, one {# hodina} few {# hodín} many {# hodín} other {# hodín}}",
   "intervals.full.minutes": "{number, plural, one {# minúta} few {# minút} many {# minút} other {# minút}}",
@@ -364,7 +364,7 @@
   "mute_modal.duration": "Trvanie",
   "mute_modal.hide_notifications": "Skry oznámenia od tohto používateľa?",
   "mute_modal.indefinite": "Bez obmedzenia",
-  "navigation_bar.about": "About",
+  "navigation_bar.about": "O tomto serveri",
   "navigation_bar.blocks": "Blokovaní užívatelia",
   "navigation_bar.bookmarks": "Záložky",
   "navigation_bar.community_timeline": "Miestna časová os",
@@ -385,7 +385,7 @@
   "navigation_bar.pins": "Pripnuté príspevky",
   "navigation_bar.preferences": "Nastavenia",
   "navigation_bar.public_timeline": "Federovaná časová os",
-  "navigation_bar.search": "Search",
+  "navigation_bar.search": "Hľadaj",
   "navigation_bar.security": "Zabezbečenie",
   "not_signed_in_indicator.not_signed_in": "You need to sign in to access this resource.",
   "notification.admin.report": "{name} nahlásil/a {target}",
@@ -455,8 +455,8 @@
   "privacy.public.short": "Verejné",
   "privacy.unlisted.long": "Visible for all, but opted-out of discovery features",
   "privacy.unlisted.short": "Verejne, ale nezobraziť v osi",
-  "privacy_policy.last_updated": "Last updated {date}",
-  "privacy_policy.title": "Privacy Policy",
+  "privacy_policy.last_updated": "Posledná úprava {date}",
+  "privacy_policy.title": "Zásady súkromia",
   "refresh": "Obnoviť",
   "regeneration_indicator.label": "Načítava sa…",
   "regeneration_indicator.sublabel": "Tvoja domovská nástenka sa pripravuje!",
@@ -532,12 +532,12 @@
   "search_results.total": "{count, number} {count, plural, one {výsledok} many {výsledkov} other {výsledky}}",
   "server_banner.about_active_users": "People using this server during the last 30 days (Monthly Active Users)",
   "server_banner.active_users": "active users",
-  "server_banner.administered_by": "Administered by:",
+  "server_banner.administered_by": "Správcom je:",
   "server_banner.introduction": "{domain} is part of the decentralized social network powered by {mastodon}.",
-  "server_banner.learn_more": "Learn more",
-  "server_banner.server_stats": "Server stats:",
-  "sign_in_banner.create_account": "Create account",
-  "sign_in_banner.sign_in": "Sign in",
+  "server_banner.learn_more": "Zisti viac",
+  "server_banner.server_stats": "Serverové štatistiky:",
+  "sign_in_banner.create_account": "Vytvor účet",
+  "sign_in_banner.sign_in": "Prihlás sa",
   "sign_in_banner.text": "Sign in to follow profiles or hashtags, favourite, share and reply to posts, or interact from your account on a different server.",
   "status.admin_account": "Otvor moderovacie rozhranie užívateľa @{name}",
   "status.admin_status": "Otvor tento príspevok v moderovacom rozhraní",
@@ -575,7 +575,7 @@
   "status.reblogs.empty": "Nikto ešte nevyzdvihol tento príspevok. Keď tak niekto urobí, bude to zobrazené práve tu.",
   "status.redraft": "Vymaž a prepíš",
   "status.remove_bookmark": "Odstráň záložku",
-  "status.replied_to": "Replied to {name}",
+  "status.replied_to": "Odpoveď na {name}",
   "status.reply": "Odpovedať",
   "status.replyAll": "Odpovedz na diskusiu",
   "status.report": "Nahlás @{name}",
@@ -586,14 +586,14 @@
   "status.show_less_all": "Všetkým ukáž menej",
   "status.show_more": "Ukáž viac",
   "status.show_more_all": "Všetkým ukáž viac",
-  "status.show_original": "Show original",
-  "status.translate": "Translate",
+  "status.show_original": "Ukáž pôvodný",
+  "status.translate": "Preložiť",
   "status.translated_from_with": "Translated from {lang} using {provider}",
   "status.uncached_media_warning": "Nedostupný/é",
   "status.unmute_conversation": "Prestaň si nevšímať konverzáciu",
   "status.unpin": "Odopni z profilu",
   "subscribed_languages.lead": "Only posts in selected languages will appear on your home and list timelines after the change. Select none to receive posts in all languages.",
-  "subscribed_languages.save": "Save changes",
+  "subscribed_languages.save": "Ulož zmeny",
   "subscribed_languages.target": "Change subscribed languages for {target}",
   "suggestions.dismiss": "Zavrhni návrh",
   "suggestions.header": "Mohlo by ťa zaujímať…",
diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json
index cdcca1697..860c0e5a1 100644
--- a/app/javascript/mastodon/locales/sq.json
+++ b/app/javascript/mastodon/locales/sq.json
@@ -2,7 +2,7 @@
   "about.blocks": "Shërbyes të moderuar",
   "about.contact": "Kontakt:",
   "about.disclaimer": "Mastodon-i është software i lirë, me burim të hapët dhe shenjë tregtare e Mastodon gGmbH.",
-  "about.domain_blocks.no_reason_available": "Reason not available",
+  "about.domain_blocks.no_reason_available": "S’ka arsye",
   "about.domain_blocks.preamble": "Mastodon-i ju lë përgjithësisht të shihni lëndë prej përdoruesish dhe të ndërveproni me ta nga cilido shërbyes tjetër qofshin në fedivers. Ka përjashtime që janë bërë në këtë shërbyes të dhënë.",
   "about.domain_blocks.silenced.explanation": "Përgjithësisht s’do të shihni profile dhe lëndë nga ky shërbyes, veç në i kërkofshi shprehimisht apo zgjidhni të bëhet kjo, duke i ndjekur.",
   "about.domain_blocks.silenced.title": "E kufizuar",
@@ -49,7 +49,7 @@
   "account.mute": "Heshtoni @{name}",
   "account.mute_notifications": "Heshtoji njoftimet prej @{name}",
   "account.muted": "Heshtuar",
-  "account.open_original_page": "Open original page",
+  "account.open_original_page": "Hap faqen origjinale",
   "account.posts": "Mesazhe",
   "account.posts_with_replies": "Mesazhe dhe përgjigje",
   "account.report": "Raportojeni @{name}",
@@ -292,7 +292,7 @@
   "interaction_modal.description.reply": "Me një llogari në Mastodon, mund t’i përgjigjeni këtij postimi.",
   "interaction_modal.on_another_server": "Në një tjetër shërbyes",
   "interaction_modal.on_this_server": "Në këtë shërbyes",
-  "interaction_modal.other_server_instructions": "Copy and paste this URL into the search field of your favourite Mastodon app or the web interface of your Mastodon server.",
+  "interaction_modal.other_server_instructions": "Kopjojeni dhe ngjiteni këtë URL te fusha e kërkimeve të aplikacionit tuaj të parapëlqyer Mastodon, ose të ndërfaqes web të shërbyesit tuaj Mastodon.",
   "interaction_modal.preamble": "Ngaqë Mastodon-i është i decentralizuar, mund të përdorni llogarinë tuaj ekzistuese të sterhuar nga një tjetër shërbyes Mastodon, ose platformë e përputhshme, nëse s’keni një llogari në këtë shërbyes.",
   "interaction_modal.title.favourite": "Parapëlqejeni postimin e {name}",
   "interaction_modal.title.follow": "Ndiq {name}",
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index 77279ba01..d6d53d56c 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -4,11 +4,11 @@
   "about.disclaimer": "Mastodon เป็นซอฟต์แวร์เสรี โอเพนซอร์ส และเครื่องหมายการค้าของ Mastodon gGmbH",
   "about.domain_blocks.no_reason_available": "เหตุผลไม่พร้อมใช้งาน",
   "about.domain_blocks.preamble": "โดยทั่วไป Mastodon อนุญาตให้คุณดูเนื้อหาจากและโต้ตอบกับผู้ใช้จากเซิร์ฟเวอร์อื่นใดในจักรวาลสหพันธ์ นี่คือข้อยกเว้นที่ทำขึ้นในเซิร์ฟเวอร์นี้โดยเฉพาะ",
-  "about.domain_blocks.silenced.explanation": "You will generally not see profiles and content from this server, unless you explicitly look it up or opt into it by following.",
+  "about.domain_blocks.silenced.explanation": "โดยทั่วไปคุณจะไม่เห็นโปรไฟล์และเนื้อหาจากเซิร์ฟเวอร์นี้ เว้นแต่คุณจะค้นหาเซิร์ฟเวอร์หรือเลือกรับเซิร์ฟเวอร์โดยการติดตามอย่างชัดเจน",
   "about.domain_blocks.silenced.title": "จำกัดอยู่",
   "about.domain_blocks.suspended.explanation": "จะไม่ประมวลผล จัดเก็บ หรือแลกเปลี่ยนข้อมูลจากเซิร์ฟเวอร์นี้ ทำให้การโต้ตอบหรือการสื่อสารใด ๆ กับผู้ใช้จากเซิร์ฟเวอร์นี้เป็นไปไม่ได้",
   "about.domain_blocks.suspended.title": "ระงับอยู่",
-  "about.not_available": "This information has not been made available on this server.",
+  "about.not_available": "ไม่ได้ทำให้ข้อมูลนี้พร้อมใช้งานในเซิร์ฟเวอร์นี้",
   "about.powered_by": "สื่อสังคมแบบกระจายศูนย์ที่ขับเคลื่อนโดย {mastodon}",
   "about.rules": "กฎของเซิร์ฟเวอร์",
   "account.account_note_header": "หมายเหตุ",
@@ -93,9 +93,9 @@
   "bundle_modal_error.message": "มีบางอย่างผิดพลาดขณะโหลดส่วนประกอบนี้",
   "bundle_modal_error.retry": "ลองอีกครั้ง",
   "closed_registrations.other_server_instructions": "เนื่องจาก Mastodon เป็นแบบกระจายศูนย์ คุณสามารถสร้างบัญชีในเซิร์ฟเวอร์อื่นและยังคงโต้ตอบกับเซิร์ฟเวอร์นี้",
-  "closed_registrations_modal.description": "Creating an account on {domain} is currently not possible, but please keep in mind that you do not need an account specifically on {domain} to use Mastodon.",
+  "closed_registrations_modal.description": "ไม่สามารถสร้างบัญชีใน {domain} ได้ในปัจจุบัน แต่โปรดจำไว้ว่าคุณไม่จำเป็นต้องมีบัญชีใน {domain} โดยเฉพาะเพื่อใช้ Mastodon",
   "closed_registrations_modal.find_another_server": "ค้นหาเซิร์ฟเวอร์อื่น",
-  "closed_registrations_modal.preamble": "Mastodon is decentralized, so no matter where you create your account, you will be able to follow and interact with anyone on this server. You can even self-host it!",
+  "closed_registrations_modal.preamble": "Mastodon เป็นแบบกระจายศูนย์ ดังนั้นไม่ว่าคุณจะสร้างบัญชีของคุณที่ใด คุณจะสามารถติดตามและโต้ตอบกับใครก็ตามในเซิร์ฟเวอร์นี้ คุณยังสามารถโฮสต์บัญชีด้วยตนเองได้อีกด้วย!",
   "closed_registrations_modal.title": "การลงทะเบียนใน Mastodon",
   "column.about": "เกี่ยวกับ",
   "column.blocks": "ผู้ใช้ที่ปิดกั้นอยู่",
@@ -127,7 +127,7 @@
   "compose.language.search": "ค้นหาภาษา...",
   "compose_form.direct_message_warning_learn_more": "เรียนรู้เพิ่มเติม",
   "compose_form.encryption_warning": "โพสต์ใน Mastodon ไม่ได้เข้ารหัสแบบต้นทางถึงปลายทาง อย่าแบ่งปันข้อมูลที่ละเอียดอ่อนใด ๆ ผ่าน Mastodon",
-  "compose_form.hashtag_warning": "จะไม่แสดงรายการโพสต์นี้ภายใต้แฮชแท็กใด ๆ เนื่องจากไม่อยู่ในรายการ เฉพาะโพสต์สาธารณะเท่านั้นที่สามารถค้นหาได้โดยแฮชแท็ก",
+  "compose_form.hashtag_warning": "จะไม่แสดงรายการโพสต์นี้ภายใต้แฮชแท็กใด ๆ เนื่องจากโพสต์ไม่อยู่ในรายการ เฉพาะโพสต์สาธารณะเท่านั้นที่สามารถค้นหาได้โดยแฮชแท็ก",
   "compose_form.lock_disclaimer": "บัญชีของคุณไม่ได้ {locked} ใครก็ตามสามารถติดตามคุณเพื่อดูโพสต์สำหรับผู้ติดตามเท่านั้นของคุณ",
   "compose_form.lock_disclaimer.lock": "ล็อคอยู่",
   "compose_form.placeholder": "คุณกำลังคิดอะไรอยู่?",
@@ -239,7 +239,7 @@
   "explore.trending_links": "ข่าว",
   "explore.trending_statuses": "โพสต์",
   "explore.trending_tags": "แฮชแท็ก",
-  "filter_modal.added.context_mismatch_explanation": "This filter category does not apply to the context in which you have accessed this post. If you want the post to be filtered in this context too, you will have to edit the filter.",
+  "filter_modal.added.context_mismatch_explanation": "หมวดหมู่ตัวกรองนี้ไม่ได้นำไปใช้กับบริบทที่คุณได้เข้าถึงโพสต์นี้ หากคุณต้องการกรองโพสต์ในบริบทนี้ด้วย คุณจะต้องแก้ไขตัวกรอง",
   "filter_modal.added.context_mismatch_title": "บริบทไม่ตรงกัน!",
   "filter_modal.added.expired_explanation": "หมวดหมู่ตัวกรองนี้หมดอายุแล้ว คุณจะต้องเปลี่ยนวันหมดอายุสำหรับหมวดหมู่เพื่อนำไปใช้",
   "filter_modal.added.expired_title": "ตัวกรองหมดอายุแล้ว!",
@@ -496,7 +496,7 @@
   "report.reasons.spam": "โพสต์เป็นสแปม",
   "report.reasons.spam_description": "ลิงก์ที่เป็นอันตราย, การมีส่วนร่วมปลอม หรือการตอบกลับซ้ำ ๆ",
   "report.reasons.violation": "โพสต์ละเมิดกฎของเซิร์ฟเวอร์",
-  "report.reasons.violation_description": "คุณทราบว่าโพสต์แหกกฎเฉพาะ",
+  "report.reasons.violation_description": "คุณตระหนักว่าโพสต์แหกกฎเฉพาะ",
   "report.rules.subtitle": "เลือกทั้งหมดที่นำไปใช้",
   "report.rules.title": "กำลังละเมิดกฎใด?",
   "report.statuses.subtitle": "เลือกทั้งหมดที่นำไปใช้",
diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json
index 1ed5e5872..9dd5aef76 100644
--- a/app/javascript/mastodon/locales/tr.json
+++ b/app/javascript/mastodon/locales/tr.json
@@ -2,7 +2,7 @@
   "about.blocks": "Denetlenen sunucular",
   "about.contact": "İletişim:",
   "about.disclaimer": "Mastodon özgür, açık kaynak bir yazılımdır ve Mastodon gGmbH şirketinin ticari markasıdır.",
-  "about.domain_blocks.no_reason_available": "Grerekçe mevcut değil",
+  "about.domain_blocks.no_reason_available": "Gerekçe mevcut değil",
   "about.domain_blocks.preamble": "Mastodon, genel olarak fediverse'teki herhangi bir sunucudan içerik görüntülemenize ve kullanıcılarıyla etkileşim kurmanıza izin verir. Bunlar, bu sunucuda yapılmış olan istisnalardır.",
   "about.domain_blocks.silenced.explanation": "Açık bir şekilde aramadığınız veya takip ederek abone olmadığınız sürece, bu sunucudaki profilleri veya içerikleri genelde göremeyeceksiniz.",
   "about.domain_blocks.silenced.title": "Sınırlı",
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js
index afb8b40c1..8cc7bf520 100644
--- a/app/javascript/mastodon/reducers/compose.js
+++ b/app/javascript/mastodon/reducers/compose.js
@@ -436,7 +436,7 @@ export default function compose(state = initialState, action) {
       .setIn(['media_modal', 'dirty'], false)
       .update('media_attachments', list => list.map(item => {
         if (item.get('id') === action.media.id) {
-          return fromJS(action.media);
+          return fromJS(action.media).set('unattached', true);
         }
 
         return item;
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 2c339ffef..24e2aed5d 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -964,7 +964,7 @@
 }
 
 .status__content.status__content--collapsed {
-  max-height: 20px * 15; // 15 lines is roughly above 500 characters
+  max-height: 22px * 15; // 15 lines is roughly above 500 characters
 }
 
 .status__content__read-more-button {
@@ -1869,9 +1869,6 @@ a.account__display-name {
 
   a {
     color: inherit;
-  }
-
-  .permalink {
     text-decoration: none;
   }
 
@@ -7026,8 +7023,11 @@ noscript {
   &__tabs {
     display: flex;
     align-items: flex-start;
+    justify-content: space-between;
     margin-top: -55px;
     padding-top: 10px;
+    gap: 8px;
+    overflow: hidden;
 
     &__buttons {
       display: flex;
@@ -7036,6 +7036,15 @@ noscript {
       padding-top: 55px;
       overflow: hidden;
 
+      .button {
+        flex-shrink: 1;
+        white-space: nowrap;
+
+        @media screen and (max-width: $no-gap-breakpoint) {
+          min-width: 0;
+        }
+      }
+
       .icon-button {
         border: 1px solid lighten($ui-base-color, 12%);
         border-radius: 4px;
diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss
index 39211910f..b644b38f1 100644
--- a/app/javascript/styles/mastodon/tables.scss
+++ b/app/javascript/styles/mastodon/tables.scss
@@ -178,6 +178,9 @@ a.table-action-link {
   }
 
   &__toolbar {
+    position: sticky;
+    top: 0;
+    z-index: 1;
     border: 1px solid darken($ui-base-color, 8%);
     background: $ui-base-color;
     border-radius: 4px 0 0;