about summary refs log tree commit diff
path: root/app/javascript/flavours
diff options
context:
space:
mode:
Diffstat (limited to 'app/javascript/flavours')
-rw-r--r--app/javascript/flavours/glitch/actions/compose.js3
-rw-r--r--app/javascript/flavours/glitch/actions/statuses.js41
-rw-r--r--app/javascript/flavours/glitch/components/status.js6
-rw-r--r--app/javascript/flavours/glitch/components/status_content.js68
-rw-r--r--app/javascript/flavours/glitch/containers/status_container.js12
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/action_bar.js66
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/navigation_bar.js14
-rw-r--r--app/javascript/flavours/glitch/features/compose/containers/navigation_container.js21
-rw-r--r--app/javascript/flavours/glitch/features/status/components/detailed_status.js7
-rw-r--r--app/javascript/flavours/glitch/features/status/index.js15
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/header.js31
-rw-r--r--app/javascript/flavours/glitch/initial_state.js2
-rw-r--r--app/javascript/flavours/glitch/locales/cs.js175
-rw-r--r--app/javascript/flavours/glitch/names.yml8
-rw-r--r--app/javascript/flavours/glitch/reducers/compose.js1
-rw-r--r--app/javascript/flavours/glitch/reducers/status_lists.js30
-rw-r--r--app/javascript/flavours/glitch/reducers/statuses.js6
-rw-r--r--app/javascript/flavours/glitch/styles/components/columns.scss6
-rw-r--r--app/javascript/flavours/glitch/styles/components/drawer.scss2
-rw-r--r--app/javascript/flavours/glitch/styles/components/index.scss2
-rw-r--r--app/javascript/flavours/glitch/styles/components/modal.scss3
-rw-r--r--app/javascript/flavours/glitch/styles/components/single_column.scss2
-rw-r--r--app/javascript/flavours/glitch/styles/components/status.scss10
-rw-r--r--app/javascript/flavours/glitch/styles/statuses.scss2
-rw-r--r--app/javascript/flavours/vanilla/names.yml8
25 files changed, 497 insertions, 44 deletions
diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js
index 54909b56e..7a4af4cda 100644
--- a/app/javascript/flavours/glitch/actions/compose.js
+++ b/app/javascript/flavours/glitch/actions/compose.js
@@ -93,12 +93,13 @@ export const ensureComposeIsVisible = (getState, routerHistory) => {
   }
 };
 
-export function setComposeToStatus(status, text, spoiler_text) {
+export function setComposeToStatus(status, text, spoiler_text, content_type) {
   return{
     type: COMPOSE_SET_STATUS,
     status,
     text,
     spoiler_text,
+    content_type,
   };
 };
 
diff --git a/app/javascript/flavours/glitch/actions/statuses.js b/app/javascript/flavours/glitch/actions/statuses.js
index 5930b7a16..efb4cc33b 100644
--- a/app/javascript/flavours/glitch/actions/statuses.js
+++ b/app/javascript/flavours/glitch/actions/statuses.js
@@ -34,6 +34,11 @@ export const STATUS_FETCH_SOURCE_REQUEST = 'STATUS_FETCH_SOURCE_REQUEST';
 export const STATUS_FETCH_SOURCE_SUCCESS = 'STATUS_FETCH_SOURCE_SUCCESS';
 export const STATUS_FETCH_SOURCE_FAIL    = 'STATUS_FETCH_SOURCE_FAIL';
 
+export const STATUS_TRANSLATE_REQUEST = 'STATUS_TRANSLATE_REQUEST';
+export const STATUS_TRANSLATE_SUCCESS = 'STATUS_TRANSLATE_SUCCESS';
+export const STATUS_TRANSLATE_FAIL    = 'STATUS_TRANSLATE_FAIL';
+export const STATUS_TRANSLATE_UNDO    = 'STATUS_TRANSLATE_UNDO';
+
 export function fetchStatusRequest(id, skipLoading) {
   return {
     type: STATUS_FETCH_REQUEST,
@@ -101,7 +106,7 @@ export const editStatus = (id, routerHistory) => (dispatch, getState) => {
   api(getState).get(`/api/v1/statuses/${id}/source`).then(response => {
     dispatch(fetchStatusSourceSuccess());
     ensureComposeIsVisible(getState, routerHistory);
-    dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text));
+    dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text, response.data.content_type));
   }).catch(error => {
     dispatch(fetchStatusSourceFail(error));
   });
@@ -310,4 +315,36 @@ export function toggleStatusCollapse(id, isCollapsed) {
     id,
     isCollapsed,
   };
-}
+};
+
+export const translateStatus = id => (dispatch, getState) => {
+  dispatch(translateStatusRequest(id));
+
+  api(getState).post(`/api/v1/statuses/${id}/translate`).then(response => {
+    dispatch(translateStatusSuccess(id, response.data));
+  }).catch(error => {
+    dispatch(translateStatusFail(id, error));
+  });
+};
+
+export const translateStatusRequest = id => ({
+  type: STATUS_TRANSLATE_REQUEST,
+  id,
+});
+
+export const translateStatusSuccess = (id, translation) => ({
+  type: STATUS_TRANSLATE_SUCCESS,
+  id,
+  translation,
+});
+
+export const translateStatusFail = (id, error) => ({
+  type: STATUS_TRANSLATE_FAIL,
+  id,
+  error,
+});
+
+export const undoStatusTranslation = id => ({
+  type: STATUS_TRANSLATE_UNDO,
+  id,
+});
diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js
index 800832dc8..4041b4819 100644
--- a/app/javascript/flavours/glitch/components/status.js
+++ b/app/javascript/flavours/glitch/components/status.js
@@ -83,6 +83,7 @@ class Status extends ImmutablePureComponent {
     onEmbed: PropTypes.func,
     onHeightChange: PropTypes.func,
     onToggleHidden: PropTypes.func,
+    onTranslate: PropTypes.func,
     onInteractionModal: PropTypes.func,
     muted: PropTypes.bool,
     hidden: PropTypes.bool,
@@ -472,6 +473,10 @@ class Status extends ImmutablePureComponent {
     this.node = c;
   }
 
+  handleTranslate = () => {
+    this.props.onTranslate(this.props.status);
+  }
+
   renderLoadingMediaGallery () {
     return <div className='media-gallery' style={{ height: '110px' }} />;
   }
@@ -788,6 +793,7 @@ class Status extends ImmutablePureComponent {
             mediaIcons={contentMediaIcons}
             expanded={isExpanded}
             onExpandedToggle={this.handleExpandedToggle}
+            onTranslate={this.handleTranslate}
             parseClick={parseClick}
             disabled={!router}
             tagLinks={settings.get('tag_misleading_links')}
diff --git a/app/javascript/flavours/glitch/components/status_content.js b/app/javascript/flavours/glitch/components/status_content.js
index c618cedca..c59f42220 100644
--- a/app/javascript/flavours/glitch/components/status_content.js
+++ b/app/javascript/flavours/glitch/components/status_content.js
@@ -1,11 +1,11 @@
 import React from 'react';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import PropTypes from 'prop-types';
-import { FormattedMessage } from 'react-intl';
+import { FormattedMessage, injectIntl } from 'react-intl';
 import Permalink from './permalink';
 import classnames from 'classnames';
 import Icon from 'flavours/glitch/components/icon';
-import { autoPlayGif } from 'flavours/glitch/initial_state';
+import { autoPlayGif, languages as preloadedLanguages, translationEnabled } from 'flavours/glitch/initial_state';
 import { decode as decodeIDNA } from 'flavours/glitch/utils/idna';
 
 const textMatchesTarget = (text, origin, host) => {
@@ -62,13 +62,56 @@ const isLinkMisleading = (link) => {
   return !(textMatchesTarget(text, origin, host) || textMatchesTarget(text.toLowerCase(), origin, host));
 };
 
-export default class StatusContent extends React.PureComponent {
+class TranslateButton extends React.PureComponent {
+
+  static propTypes = {
+    translation: ImmutablePropTypes.map,
+    onClick: PropTypes.func,
+  };
+
+  render () {
+    const { translation, onClick } = this.props;
+
+    if (translation) {
+      const language     = preloadedLanguages.find(lang => lang[0] === translation.get('detected_source_language'));
+      const languageName = language ? language[2] : translation.get('detected_source_language');
+      const provider     = translation.get('provider');
+
+      return (
+        <div className='translate-button'>
+          <div className='translate-button__meta'>
+            <FormattedMessage id='status.translated_from_with' defaultMessage='Translated from {lang} using {provider}' values={{ lang: languageName, provider }} />
+          </div>
+
+          <button className='link-button' onClick={onClick}>
+            <FormattedMessage id='status.show_original' defaultMessage='Show original' />
+          </button>
+        </div>
+      );
+    }
+
+    return (
+      <button className='status__content__read-more-button' onClick={onClick}>
+        <FormattedMessage id='status.translate' defaultMessage='Translate' />
+      </button>
+    );
+  }
+
+}
+
+export default @injectIntl
+class StatusContent extends React.PureComponent {
+
+  static contextTypes = {
+    identity: PropTypes.object,
+  };
 
   static propTypes = {
     status: ImmutablePropTypes.map.isRequired,
     expanded: PropTypes.bool,
     collapsed: PropTypes.bool,
     onExpandedToggle: PropTypes.func,
+    onTranslate: PropTypes.func,
     media: PropTypes.node,
     extraMedia: PropTypes.node,
     mediaIcons: PropTypes.arrayOf(PropTypes.string),
@@ -77,6 +120,7 @@ export default class StatusContent extends React.PureComponent {
     onUpdate: PropTypes.func,
     tagLinks: PropTypes.bool,
     rewriteMentions: PropTypes.string,
+    intl: PropTypes.object,
   };
 
   static defaultProps = {
@@ -249,6 +293,10 @@ export default class StatusContent extends React.PureComponent {
     }
   }
 
+  handleTranslate = () => {
+    this.props.onTranslate();
+  }
+
   setContentsRef = (c) => {
     this.contentsNode = c;
   }
@@ -263,18 +311,24 @@ export default class StatusContent extends React.PureComponent {
       disabled,
       tagLinks,
       rewriteMentions,
+      intl,
     } = this.props;
 
     const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
+    const renderTranslate = translationEnabled && this.context.identity.signedIn && this.props.onTranslate && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('contentHtml').length > 0 && status.get('language') !== null && intl.locale !== status.get('language');
 
-    const content = { __html: status.get('contentHtml') };
+    const content = { __html: status.get('translation') ? status.getIn(['translation', 'content']) : status.get('contentHtml') };
     const spoilerContent = { __html: status.get('spoilerHtml') };
-    const lang = status.get('language');
+    const lang = status.get('translation') ? intl.locale : status.get('language');
     const classNames = classnames('status__content', {
       'status__content--with-action': parseClick && !disabled,
       'status__content--with-spoiler': status.get('spoiler_text').length > 0,
     });
 
+    const translateButton = renderTranslate && (
+      <TranslateButton onClick={this.handleTranslate} translation={status.get('translation')} />
+    );
+
     if (status.get('spoiler_text').length > 0) {
       let mentionsPlaceholder = '';
 
@@ -350,11 +404,11 @@ export default class StatusContent extends React.PureComponent {
               onMouseLeave={this.handleMouseLeave}
               lang={lang}
             />
+            {!hidden && translateButton}
             {media}
           </div>
 
           {extraMedia}
-
         </div>
       );
     } else if (parseClick) {
@@ -375,6 +429,7 @@ export default class StatusContent extends React.PureComponent {
             onMouseLeave={this.handleMouseLeave}
             lang={lang}
           />
+          {translateButton}
           {media}
           {extraMedia}
         </div>
@@ -395,6 +450,7 @@ export default class StatusContent extends React.PureComponent {
             onMouseLeave={this.handleMouseLeave}
             lang={lang}
           />
+          {translateButton}
           {media}
           {extraMedia}
         </div>
diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js
index c12b2e614..947573fc7 100644
--- a/app/javascript/flavours/glitch/containers/status_container.js
+++ b/app/javascript/flavours/glitch/containers/status_container.js
@@ -23,7 +23,9 @@ import {
   deleteStatus,
   hideStatus,
   revealStatus,
-  editStatus
+  editStatus,
+  translateStatus,
+  undoStatusTranslation,
 } from 'flavours/glitch/actions/statuses';
 import {
   initAddFilter,
@@ -187,6 +189,14 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
     dispatch(editStatus(status.get('id'), history));
   },
 
+  onTranslate (status) {
+    if (status.get('translation')) {
+      dispatch(undoStatusTranslation(status.get('id')));
+    } else {
+      dispatch(translateStatus(status.get('id')));
+    }
+  },
+
   onDirect (account, router) {
     dispatch(directCompose(account, router));
   },
diff --git a/app/javascript/flavours/glitch/features/compose/components/action_bar.js b/app/javascript/flavours/glitch/features/compose/components/action_bar.js
new file mode 100644
index 000000000..267c0ba69
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/compose/components/action_bar.js
@@ -0,0 +1,66 @@
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import PropTypes from 'prop-types';
+import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
+import { defineMessages, injectIntl } from 'react-intl';
+import { preferencesLink, profileLink } from 'flavours/glitch/utils/backend_links';
+
+const messages = defineMessages({
+  edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
+  pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' },
+  preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
+  follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
+  favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
+  lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
+  blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
+  domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' },
+  mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
+  filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' },
+  logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
+  bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
+});
+
+export default @injectIntl
+class ActionBar extends React.PureComponent {
+
+  static propTypes = {
+    account: ImmutablePropTypes.map.isRequired,
+    onLogout: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  handleLogout = () => {
+    this.props.onLogout();
+  }
+
+  render () {
+    const { intl } = this.props;
+
+    let menu = [];
+
+    menu.push({ text: intl.formatMessage(messages.edit_profile), href: profileLink });
+    menu.push({ text: intl.formatMessage(messages.preferences), href: preferencesLink });
+    menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' });
+    menu.push(null);
+    menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
+    menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' });
+    menu.push({ text: intl.formatMessage(messages.bookmarks), to: '/bookmarks' });
+    menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' });
+    menu.push(null);
+    menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
+    menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
+    menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
+    menu.push({ text: intl.formatMessage(messages.filters), href: '/filters' });
+    menu.push(null);
+    menu.push({ text: intl.formatMessage(messages.logout), action: this.handleLogout });
+
+    return (
+      <div className='compose__action-bar'>
+        <div className='compose__action-bar-dropdown'>
+          <DropdownMenuContainer items={menu} icon='ellipsis-v' size={18} direction='right' />
+        </div>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/glitch/features/compose/components/navigation_bar.js b/app/javascript/flavours/glitch/features/compose/components/navigation_bar.js
index ba73ed553..1a68f1e12 100644
--- a/app/javascript/flavours/glitch/features/compose/components/navigation_bar.js
+++ b/app/javascript/flavours/glitch/features/compose/components/navigation_bar.js
@@ -1,5 +1,7 @@
 import React from 'react';
+import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
+import ActionBar from './action_bar';
 import Avatar from 'flavours/glitch/components/avatar';
 import Permalink from 'flavours/glitch/components/permalink';
 import { FormattedMessage } from 'react-intl';
@@ -10,11 +12,12 @@ export default class NavigationBar extends ImmutablePureComponent {
 
   static propTypes = {
     account: ImmutablePropTypes.map.isRequired,
+    onLogout: PropTypes.func.isRequired,
   };
 
   render () {
     return (
-      <div className='drawer--account'>
+      <div className='navigation-bar'>
         <Permalink className='avatar' href={this.props.account.get('url')} to={`/@${this.props.account.get('acct')}`}>
           <span style={{ display: 'none' }}>{this.props.account.get('acct')}</span>
           <Avatar account={this.props.account} size={48} />
@@ -28,11 +31,16 @@ export default class NavigationBar extends ImmutablePureComponent {
           { profileLink !== undefined && (
             <a
               className='edit'
-              href={ profileLink }
+              href={profileLink}
             ><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a>
           )}
         </div>
+
+        <div className='navigation-bar__actions'>
+          <ActionBar account={this.props.account} onLogout={this.props.onLogout} />
+        </div>
       </div>
     );
-  };
+  }
+
 }
diff --git a/app/javascript/flavours/glitch/features/compose/containers/navigation_container.js b/app/javascript/flavours/glitch/features/compose/containers/navigation_container.js
index 0e1400261..89036adcd 100644
--- a/app/javascript/flavours/glitch/features/compose/containers/navigation_container.js
+++ b/app/javascript/flavours/glitch/features/compose/containers/navigation_container.js
@@ -1,11 +1,30 @@
 import { connect }   from 'react-redux';
+import { defineMessages, injectIntl } from 'react-intl';
 import NavigationBar from '../components/navigation_bar';
+import { logOut } from 'flavours/glitch/utils/log_out';
+import { openModal } from 'flavours/glitch/actions/modal';
 import { me } from 'flavours/glitch/initial_state';
 
+const messages = defineMessages({
+  logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
+  logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
+});
+
 const mapStateToProps = state => {
   return {
     account: state.getIn(['accounts', me]),
   };
 };
 
-export default connect(mapStateToProps)(NavigationBar);
+const mapDispatchToProps = (dispatch, { intl }) => ({
+  onLogout () {
+    dispatch(openModal('CONFIRM', {
+      message: intl.formatMessage(messages.logoutMessage),
+      confirm: intl.formatMessage(messages.logoutConfirm),
+      closeWhenConfirm: false,
+      onConfirm: () => logOut(),
+    }));
+  },
+});
+
+export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NavigationBar));
diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js
index 46770930f..7d2c2aace 100644
--- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js
+++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js
@@ -34,6 +34,7 @@ class DetailedStatus extends ImmutablePureComponent {
     onOpenMedia: PropTypes.func.isRequired,
     onOpenVideo: PropTypes.func.isRequired,
     onToggleHidden: PropTypes.func,
+    onTranslate: PropTypes.func.isRequired,
     expanded: PropTypes.bool,
     measureHeight: PropTypes.bool,
     onHeightChange: PropTypes.func,
@@ -112,6 +113,11 @@ class DetailedStatus extends ImmutablePureComponent {
     window.open(href, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
   }
 
+  handleTranslate = () => {
+    const { onTranslate, status } = this.props;
+    onTranslate(status);
+  }
+
   render () {
     const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
     const { expanded, onToggleHidden, settings, usingPiP, intl } = this.props;
@@ -305,6 +311,7 @@ class DetailedStatus extends ImmutablePureComponent {
             expanded={expanded}
             collapsed={false}
             onExpandedToggle={onToggleHidden}
+            onTranslate={this.handleTranslate}
             parseClick={this.parseClick}
             onUpdate={this.handleChildUpdate}
             tagLinks={settings.get('tag_misleading_links')}
diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js
index aaa9c7928..e190652b0 100644
--- a/app/javascript/flavours/glitch/features/status/index.js
+++ b/app/javascript/flavours/glitch/features/status/index.js
@@ -33,7 +33,9 @@ import {
   deleteStatus,
   editStatus,
   hideStatus,
-  revealStatus
+  revealStatus,
+  translateStatus,
+  undoStatusTranslation,
 } from 'flavours/glitch/actions/statuses';
 import { initMuteModal } from 'flavours/glitch/actions/mutes';
 import { initBlockModal } from 'flavours/glitch/actions/blocks';
@@ -437,6 +439,16 @@ class Status extends ImmutablePureComponent {
     this.setState({ isExpanded: !isExpanded, threadExpanded: !isExpanded });
   }
 
+  handleTranslate = status => {
+    const { dispatch } = this.props;
+
+    if (status.get('translation')) {
+      dispatch(undoStatusTranslation(status.get('id')));
+    } else {
+      dispatch(translateStatus(status.get('id')));
+    }
+  }
+
   handleBlockClick = (status) => {
     const { dispatch } = this.props;
     const account = status.get('account');
@@ -666,6 +678,7 @@ class Status extends ImmutablePureComponent {
                   onOpenMedia={this.handleOpenMedia}
                   expanded={isExpanded}
                   onToggleHidden={this.handleToggleHidden}
+                  onTranslate={this.handleTranslate}
                   domain={domain}
                   showMedia={this.state.showMedia}
                   onToggleMediaVisibility={this.handleToggleMediaVisibility}
diff --git a/app/javascript/flavours/glitch/features/ui/components/header.js b/app/javascript/flavours/glitch/features/ui/components/header.js
index 891f7fc07..d9ad94961 100644
--- a/app/javascript/flavours/glitch/features/ui/components/header.js
+++ b/app/javascript/flavours/glitch/features/ui/components/header.js
@@ -7,6 +7,7 @@ import Avatar from 'flavours/glitch/components/avatar';
 import Permalink from 'flavours/glitch/components/permalink';
 import PropTypes from 'prop-types';
 import { connect } from 'react-redux';
+import { openModal } from 'flavours/glitch/actions/modal';
 
 const Account = connect(state => ({
   account: state.getIn(['accounts', me]),
@@ -16,7 +17,14 @@ const Account = connect(state => ({
   </Permalink>
 ));
 
-export default @withRouter
+const mapDispatchToProps = (dispatch) => ({
+  openClosedRegistrationsModal() {
+    dispatch(openModal('CLOSED_REGISTRATIONS'));
+  },
+});
+
+export default @connect(null, mapDispatchToProps)
+@withRouter
 class Header extends React.PureComponent {
 
   static contextTypes = {
@@ -24,12 +32,13 @@ class Header extends React.PureComponent {
   };
 
   static propTypes = {
+    openClosedRegistrationsModal: PropTypes.func,
     location: PropTypes.object,
   };
 
   render () {
     const { signedIn } = this.context.identity;
-    const { location } = this.props;
+    const { location, openClosedRegistrationsModal } = this.props;
 
     let content;
 
@@ -41,10 +50,26 @@ class Header extends React.PureComponent {
         </>
       );
     } else {
+      let signupButton;
+
+      if (registrationsOpen) {
+        signupButton = (
+          <a href='/auth/sign_up' className='button button-tertiary'>
+            <FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
+          </a>
+        );
+      } else {
+        signupButton = (
+          <button className='button button-tertiary' onClick={openClosedRegistrationsModal}>
+            <FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
+          </button>
+        );
+      }
+
       content = (
         <>
           <a href='/auth/sign_in' className='button'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a>
-          <a href={registrationsOpen ? '/auth/sign_up' : 'https://joinmastodon.org/servers'} className='button button-tertiary'><FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' /></a>
+          {signupButton}
         </>
       );
     }
diff --git a/app/javascript/flavours/glitch/initial_state.js b/app/javascript/flavours/glitch/initial_state.js
index 5be177ced..bbf25c8a8 100644
--- a/app/javascript/flavours/glitch/initial_state.js
+++ b/app/javascript/flavours/glitch/initial_state.js
@@ -79,6 +79,7 @@
  * @property {boolean} use_blurhash
  * @property {boolean=} use_pending_items
  * @property {string} version
+ * @property {boolean} translation_enabled
  * @property {object} local_settings
  */
 
@@ -137,6 +138,7 @@ export const unfollowModal = getMeta('unfollow_modal');
 export const useBlurhash = getMeta('use_blurhash');
 export const usePendingItems = getMeta('use_pending_items');
 export const version = getMeta('version');
+export const translationEnabled = getMeta('translation_enabled');
 export const languages = initialState?.languages;
 
 // Glitch-soc-specific settings
diff --git a/app/javascript/flavours/glitch/locales/cs.js b/app/javascript/flavours/glitch/locales/cs.js
index ac7db0327..e789facb0 100644
--- a/app/javascript/flavours/glitch/locales/cs.js
+++ b/app/javascript/flavours/glitch/locales/cs.js
@@ -1,7 +1,180 @@
 import inherited from 'mastodon/locales/cs.json';
 
 const messages = {
-  //  No translations available.
+  'about.fork_disclaimer': 'Glitch-soc je svobodný software s otevřeným zdrojovým kódem založený na Mastodonu.',
+  'settings.layout_opts': 'Možnosti rozvržení',
+  'settings.layout': 'Rozložení:',
+  'layout.current_is': 'Nastavené rozložení je:',
+  'layout.auto': 'Automatické',
+  'layout.desktop': 'Desktop',
+  'layout.mobile': 'Mobil',
+  'layout.hint.auto': 'Vybrat rozložení automaticky v závislosti na nastavení “Povolit pokročilé webové rozhraní” a velikosti obrazovky.',
+  'layout.hint.desktop': 'Použít vícesloupcové rozložení nezávisle na nastavení “Povolit pokročilé webové rozhraní” a velikosti obrazovky.',
+  'layout.hint.single': 'Použít jednosloupcové rozložení nezávisle na nastavení “Povolit pokročilé webové rozhraní” a velikosti obrazovky.',
+  'navigation_bar.app_settings': 'Nastavení aplikace',
+  'navigation_bar.featured_users': 'Vybraní uživatelé',
+  'endorsed_accounts_editor.endorsed_accounts': 'Vybrané účty',
+  'navigation_bar.info': 'Rozšířené informace',
+  'navigation_bar.misc': 'Různé',
+  'navigation_bar.keyboard_shortcuts': 'Klávesové zkratky',
+  'getting_started.onboarding': 'Ukaž mi to tu', 
+  'onboarding.skip': 'Přeskočit',
+  'onboarding.next': 'Další',
+  'onboarding.done': 'Hotovo',
+  'onboarding.page_one.federation': '{domain} je \'instance\' Mastodonu. Mastodon je síť nezávislých serverů, které jsou spolu propojené do jedné velké sociální sítě. Těmto serverům říkáme instance.',
+  'onboarding.page_one.handle': 'Jste na instanci {domain}, takže celá adresa vašeho profilu je {handle}',
+  'onboarding.page_one.welcome': 'Vítá vás {domain}!',
+  'onboarding.page_two.compose': 'Příspěvky se píší v levém sloupci. Pomocí ikon pod příspěvkem k němu můžete připojit obrázky, změnit úroveň soukromí nebo přidat varování o obsahu.',
+  'onboarding.page_three.search': 'Pomocí vyhledávací lišty můžete hledat lidi nebo hashtagy. Pokud hledáte někoho z jiné instance, musíte použít celou adresu jeho profilu.',
+  'onboarding.page_three.profile': 'Upravte si svůj profil a nastavte si profilový obrázek, jméno, a krátký text o sobě. Naleznete tam i další možnosti nastavení.',
+  'onboarding.page_four.home': 'Domovská časová osa zobrazuje příspěvky od lidí, které sledujete.',
+  'onboarding.page_four.notifications': 'Notifikace se zobrazí, když s vámi někdo interaguje.',
+  'onboarding.page_five.public_timelines': 'Místní časová osa zobrazuje veřejné příspěvky všech uživatelů instance {domain}. Federovaná časová osa zobrazí příspěvky od všech, koho uživatelé instance {domain} sledují. Tyto veřejné časové osy jsou skvělý způsob, jak objevit nové lidi.',
+  'onboarding.page_six.almost_done': 'Skoro hotovo...',
+  'onboarding.page_six.github': 'Na serveru {domain} běží Glitchsoc. Glitchsoc je přátelský {fork} programu {Mastodon}, a je kompatibilní s jakoukoliv jinou mastodoní instancí nebo aplikací. Glitchsoc je zcela svobodný a má otevřený zdrojový kód. Na stránce {github} můžete hlásit chyby, žádat o nové funkce, nebo ke kódu vlastnoručně přispět.',
+  'onboarding.page_six.apps_available': 'Jsou dostupné {apps} pro iOS, Android i jiné platformy.',
+  'onboarding.page_six.various_app': 'mobilní aplikace',
+  'onboarding.page_six.appetoot': 'Veselé mastodonění!',
+  'settings.auto_collapse': 'Automaticky sbalit',
+  'settings.auto_collapse_all': 'Všechno',
+  'settings.auto_collapse_lengthy': 'Dlouhé příspěvky',
+  'settings.auto_collapse_media': 'Příspěvky s přílohami',
+  'settings.auto_collapse_notifications': 'Oznámení',
+  'settings.auto_collapse_reblogs': 'Boosty',
+  'settings.auto_collapse_replies': 'Odpovědi',
+  'settings.show_action_bar': 'Zobrazit ve sbalených příspěvcích tlačítka s akcemi',
+  'settings.close': 'Zavřít',
+  'settings.collapsed_statuses': 'Sbalené příspěvky',
+  'settings.confirm_boost_missing_media_description': 'Zobrazit potvrzovací dialog před boostnutím příspěvku s chybějícími popisky obrázků',
+  'boost_modal.missing_description': 'Příspěvek obsahuje obrázky bez popisků',
+  'settings.enable_collapsed': 'Povolit sbalené příspěvky',
+  'settings.enable_collapsed_hint': 'U sbalených příspěvků je část jejich obsahu skrytá, aby zabraly méně místa na obrazovce. (Tohle není stejná funkce jako varování o obsahu.)',
+  'settings.general': 'Obecné',
+  'settings.hicolor_privacy_icons': 'Barevné ikony soukromí',
+  'settings.hicolor_privacy_icons.hint': 'Zobrazit ikony úrovně soukromí příspěvků v jasných, snadno rozlišitelných barvách',
+  'settings.image_backgrounds': 'Obrázkové pozadí',
+  'settings.image_backgrounds_media': 'Náhled médií ve sbalených příspěvcích',
+  'settings.image_backgrounds_media_hint': 'Pokud jsou k příspěvku přiložena média, použije se první z nich jako pozadí', 
+  'settings.image_backgrounds_users': 'Nastavit sbaleným příspěvkům obrázkové pozadí',
+  'settings.inline_preview_cards': 'Zobrazit v časové ose náhledy externích odkazů',
+  'settings.media': 'Média',
+  'settings.media_letterbox': 'Neořezávat obrázky',
+  'settings.media_letterbox_hint': 'Místo výřezu obrázku zobrazit obrázek celý, doplněný podle potřeby o prázdné okraje',
+  'settings.media_fullwidth': 'Zobrazit náhledy v plné šířce',
+  'settings.notifications_opts': 'Možnosti oznámení',
+  'settings.notifications.tab_badge': 'Zobrazit počet nepřečtených oznámení',
+  'settings.notifications.tab_badge.hint': 'Počet nepřečtených oznámení se viditelně zobrazí na hlavní stránce (pokud není seznam oznámení viditelný)',
+  'settings.notifications.favicon_badge': 'Zobrazit počet na ikoně serveru',
+  'settings.notifications.favicon_badge.hint': 'Zobrazí počet nepřečtených oznámení na ikoně serveru',
+  'settings.preferences': 'Předvolby',
+  'settings.rewrite_mentions': 'Přepsat zmínky v zobrazených příspěvcích',
+  'settings.rewrite_mentions_no': 'Nepřepisovat zmínky',
+  'settings.rewrite_mentions_acct': 'Přepsat uživatelským jménem a doménou (pokud je účet na jiném serveru)',
+  'settings.rewrite_mentions_username': 'Přepsat uživatelským jménem',
+  'settings.show_reply_counter': 'Zobrazit odhad počtu odpovědí',
+  'settings.status_icons': 'Ikony u příspěvků',
+  'settings.status_icons_language': 'Indikace jazyk',
+  'settings.status_icons_reply': 'Indikace odpovědi',
+  'settings.status_icons_local_only': 'Indikace lokálního příspěvku',
+  'settings.status_icons_media': 'Indikace obrázků a anket',
+  'settings.status_icons_visibility': 'Indikace úrovně soukromí',
+  'settings.tag_misleading_links': 'Označit zavádějící odkazy',
+  'settings.tag_misleading_links.hint': 'Zobrazit skutečný cíl u každého odkazu, který ho explicitně nezmiňuje',
+  'settings.wide_view': 'Široké sloupce (pouze v režimu Desktop)',
+  'settings.wide_view_hint': 'Sloupce se roztáhnout, aby lépe vyplnily dostupný prostor.',
+  'settings.navbar_under': 'Navigační lišta vespod (pouze v režimu Mobil)',
+  'settings.compose_box_opts': 'Editační pole',
+  'settings.always_show_spoilers_field': 'Vždy zobrazit pole pro varování o obsahu',
+  'settings.prepend_cw_re': 'Při odpovídání přidat před varování o obsahu “re: ”',
+  'settings.preselect_on_reply': 'Při odpovědi označit uživatelská jména',
+  'settings.preselect_on_reply_hint': 'Při odpovídání na konverzaci s více účastníky se jména všech kromě prvního označí, aby šla jednoduše smazat',
+  'settings.confirm_missing_media_description': 'Zobrazit potvrzovací dialog při odesílání příspěvku, ve kterém chybí popisky obrázků',
+  'settings.confirm_before_clearing_draft': 'Zobrazit potvrzovací dialog před přepsáním právě vytvářené zprávy',
+  'settings.show_content_type_choice': 'Zobrazit volbu formátu příspěvku',
+  'settings.side_arm': 'Vedlejší odesílací tlačítko:',
+  'settings.side_arm.none': 'Žádné',
+  'settings.side_arm_reply_mode': 'Při odpovídání na příspěvek by vedlejší odesílací tlačítko mělo:',  
+  'settings.side_arm_reply_mode.keep': 'Použít svou nastavenou úroveň soukromí',
+  'settings.side_arm_reply_mode.copy': 'Použít úroveň soukromí příspěvku, na který odpovídáte',
+  'settings.side_arm_reply_mode.restrict': 'Zvýšit úroveň soukromí nejméně na úroveň příspěvku, na který odpovídáte',  
+  'settings.content_warnings': 'Varování o obsahu',
+  'settings.content_warnings_shared_state': 'Zobrazit/schovat všechny kopie naráz',
+  'settings.content_warnings_shared_state_hint': 'Tlačítko varování o obsahu bude mít efekt na všechny kopie příspěvku naráz, stejně jako na běžném Mastodonu. Nebude pak možné automaticky sbalit jakoukoliv kopii příspěvku, která má rozbalené varování o obsahu',
+  'settings.content_warnings_media_outside': 'Zobrazit obrázky a videa mimo varování o obsahu',
+  'settings.content_warnings_media_outside_hint': 'Obrázky a videa z příspěvku s varováním o obsahu se zobrazí se separátním přepínačem zobrazení, stejně jako na běžném Mastodonu.',
+  'settings.content_warnings_unfold_opts': 'Možnosti automatického rozbalení',
+  'settings.enable_content_warnings_auto_unfold': 'Vždy rozbalit příspěvky označené varováním o obsahu',
+  'settings.deprecated_setting': 'Tato možnost se nyní nastavuje v {settings_page_link}',
+  'settings.shared_settings_link': 'předvolbách Mastodonu',
+  'settings.content_warnings_filter': 'Tato varování o obsahu automaticky nerozbalovat:',
+  'settings.content_warnings.regexp': 'Regulární výraz',
+  'settings.media_reveal_behind_cw': 'Automaticky zobrazit média označená varováním o obsahu',
+  'settings.pop_in_player': 'Povolit plovoucí okno přehrávače',
+  'settings.pop_in_position': 'Pozice plovoucího okna:',
+  'settings.pop_in_left': 'Vlevo',
+  'settings.pop_in_right': 'Vpravo',
+ 
+  
+  'status.collapse': 'Sbalit',
+  'status.uncollapse': 'Rozbalit',  
+  'status.in_reply_to': 'Tento příspěvek je odpověď',
+  'status.has_preview_card': 'Obsahuje náhled odkazu',
+  'status.has_pictures': 'Obsahuje obrázky',
+  'status.is_poll': 'Tento příspěvek je anketa',
+  'status.has_video': 'Obsahuje video',
+  'status.has_audio': 'Obsahuje audio',
+  'status.local_only': 'Viditelné pouze z vaší instance',
+
+  'media_gallery.sensitive': 'Citlivý obsah',
+
+  'favourite_modal.combo': 'Příště můžete pro přeskočení stisknout {combo}',
+
+  'home.column_settings.show_direct': 'Zobrazit přímé zprávy',
+
+  'notification_purge.start': 'Čistící režim',
+  'notifications.mark_as_read': 'Označit všechna oznámení jako přečtená',
+
+  'notification.markForDeletion': 'Označit pro smazání',
+  'notifications.clear': 'Vymazat všechna oznámení',
+  'notifications.marked_clear_confirmation': 'Určitě chcete trvale smazat všechna vybraná oznámení?',
+  'notifications.marked_clear': 'Smazat vybraná oznámení',
+
+  'notification_purge.btn_all': 'Vybrat\nvše',
+  'notification_purge.btn_none': 'Nevybrat\nnic',
+  'notification_purge.btn_invert': 'Obrátit\nvýběr',
+  'notification_purge.btn_apply': 'Smazat\nvybrané',
+
+  'compose.attach.upload': 'Nahrát soubor',
+  'compose.attach.doodle': 'Něco namalovat',
+  'compose.attach': 'Připojit...',
+
+  'advanced_options.local-only.short': 'Lokální příspěvek',
+  'advanced_options.local-only.long': 'Neposílat na jiné servery',
+  'advanced_options.local-only.tooltip': 'Tento příspěvek je pouze lokální',
+  'advanced_options.icon_title': 'Pokročilá nastavení',
+  'advanced_options.threaded_mode.short': 'Režim vlákna',
+  'advanced_options.threaded_mode.long': 'Po odeslání automaticky otevře pole pro odpověď',
+  'advanced_options.threaded_mode.tooltip': 'Režim vlákna je zapnutý',
+  
+  'home.column_settings.advanced': 'Pokročilé',
+  'home.column_settings.filter_regex': 'Filtrovat podle regulárních výrazů',
+  
+  'compose_form.poll.single_choice': 'Povolit jednu odpověď',
+  'compose_form.poll.multiple_choices': 'Povolit více odpovědí',
+  
+  'compose.content-type.plain': 'Prostý text',
+  'content-type.change': 'Formát příspěvku',
+  'compose_form.spoiler': 'Přidat varování o obsahu',
+  
+  'direct.group_by_conversations': 'Seskupit do konverzací',
+  'column.toot': 'Příspěvky a odpovědi',
+  'confirmation_modal.do_not_ask_again': 'Příště se už neptat',
+  
+  'keyboard_shortcuts.bookmark': 'Přidat do záložek',
+  'keyboard_shortcuts.toggle_collapse': 'Sbalit/rozbalit příspěvek',
+  'keyboard_shortcuts.secondary_toot': 'Odeslat příspěvek s druhotným nastavením soukromí',
+  
+  'column.subheading': 'Různé',
 };
 
 export default Object.assign({}, inherited, messages);
diff --git a/app/javascript/flavours/glitch/names.yml b/app/javascript/flavours/glitch/names.yml
index 68db3a73e..f35b457e1 100644
--- a/app/javascript/flavours/glitch/names.yml
+++ b/app/javascript/flavours/glitch/names.yml
@@ -6,6 +6,14 @@ en:
   skins:
     glitch:
       default: Default
+cs:
+  flavours:
+    glitch:
+      description: Výchozí rozhraní instancí GlitchSoc.
+      name: Glitch
+  skins:
+    glitch:
+      default: Výchozí
 pl:
   flavours:
     glitch:
diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js
index b1c792406..9b50ec23a 100644
--- a/app/javascript/flavours/glitch/reducers/compose.js
+++ b/app/javascript/flavours/glitch/reducers/compose.js
@@ -599,6 +599,7 @@ export default function compose(state = initialState, action) {
     return state.withMutations(map => {
       map.set('id', action.status.get('id'));
       map.set('text', action.text);
+      map.set('content_type', action.content_type || 'text/plain');
       map.set('in_reply_to', action.status.get('in_reply_to_id'));
       map.set('privacy', action.status.get('visibility'));
       map.set('media_attachments', action.status.get('media_attachments'));
diff --git a/app/javascript/flavours/glitch/reducers/status_lists.js b/app/javascript/flavours/glitch/reducers/status_lists.js
index ada0484f4..7ac0dab47 100644
--- a/app/javascript/flavours/glitch/reducers/status_lists.js
+++ b/app/javascript/flavours/glitch/reducers/status_lists.js
@@ -25,7 +25,7 @@ import {
   TRENDS_STATUSES_EXPAND_SUCCESS,
   TRENDS_STATUSES_EXPAND_FAIL,
 } from 'flavours/glitch/actions/trends';
-import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
+import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
 import {
   FAVOURITE_SUCCESS,
   UNFAVOURITE_SUCCESS,
@@ -43,22 +43,22 @@ const initialState = ImmutableMap({
   favourites: ImmutableMap({
     next: null,
     loaded: false,
-    items: ImmutableList(),
+    items: ImmutableOrderedSet(),
   }),
   bookmarks: ImmutableMap({
     next: null,
     loaded: false,
-    items: ImmutableList(),
+    items: ImmutableOrderedSet(),
   }),
   pins: ImmutableMap({
     next: null,
     loaded: false,
-    items: ImmutableList(),
+    items: ImmutableOrderedSet(),
   }),
   trending: ImmutableMap({
     next: null,
     loaded: false,
-    items: ImmutableList(),
+    items: ImmutableOrderedSet(),
   }),
 });
 
@@ -67,7 +67,7 @@ const normalizeList = (state, listType, statuses, next) => {
     map.set('next', next);
     map.set('loaded', true);
     map.set('isLoading', false);
-    map.set('items', ImmutableList(statuses.map(item => item.id)));
+    map.set('items', ImmutableOrderedSet(statuses.map(item => item.id)));
   }));
 };
 
@@ -75,20 +75,22 @@ const appendToList = (state, listType, statuses, next) => {
   return state.update(listType, listMap => listMap.withMutations(map => {
     map.set('next', next);
     map.set('isLoading', false);
-    map.set('items', map.get('items').concat(statuses.map(item => item.id)));
+    map.set('items', map.get('items').union(statuses.map(item => item.id)));
   }));
 };
 
 const prependOneToList = (state, listType, status) => {
-  return state.update(listType, listMap => listMap.withMutations(map => {
-    map.set('items', map.get('items').unshift(status.get('id')));
-  }));
+  return state.updateIn([listType, 'items'], (list) => {
+    if (list.includes(status.get('id'))) {
+      return list;
+    } else {
+      return ImmutableOrderedSet([status.get('id')]).union(list);
+    }
+  });
 };
 
 const removeOneFromList = (state, listType, status) => {
-  return state.update(listType, listMap => listMap.withMutations(map => {
-    map.set('items', map.get('items').filter(item => item !== status.get('id')));
-  }));
+  return state.updateIn([listType, 'items'], (list) => list.delete(status.get('id')));
 };
 
 export default function statusLists(state = initialState, action) {
@@ -139,7 +141,7 @@ export default function statusLists(state = initialState, action) {
     return removeOneFromList(state, 'pins', action.status);
   case ACCOUNT_BLOCK_SUCCESS:
   case ACCOUNT_MUTE_SUCCESS:
-    return state.updateIn(['trending', 'items'], ImmutableList(), list => list.filterNot(statusId => action.statuses.getIn([statusId, 'account']) === action.relationship.id));
+    return state.updateIn(['trending', 'items'], ImmutableOrderedSet(), list => list.filterNot(statusId => action.statuses.getIn([statusId, 'account']) === action.relationship.id));
   default:
     return state;
   }
diff --git a/app/javascript/flavours/glitch/reducers/statuses.js b/app/javascript/flavours/glitch/reducers/statuses.js
index b47155c5f..f0c4c804b 100644
--- a/app/javascript/flavours/glitch/reducers/statuses.js
+++ b/app/javascript/flavours/glitch/reducers/statuses.js
@@ -13,6 +13,8 @@ import {
   STATUS_REVEAL,
   STATUS_HIDE,
   STATUS_COLLAPSE,
+  STATUS_TRANSLATE_SUCCESS,
+  STATUS_TRANSLATE_UNDO,
   STATUS_FETCH_REQUEST,
   STATUS_FETCH_FAIL,
 } from 'flavours/glitch/actions/statuses';
@@ -85,6 +87,10 @@ export default function statuses(state = initialState, action) {
     return state.setIn([action.id, 'collapsed'], action.isCollapsed);
   case TIMELINE_DELETE:
     return deleteStatus(state, action.id, action.references);
+  case STATUS_TRANSLATE_SUCCESS:
+    return state.setIn([action.id, 'translation'], fromJS(action.translation));
+  case STATUS_TRANSLATE_UNDO:
+    return state.deleteIn([action.id, 'translation']);
   default:
     return state;
   }
diff --git a/app/javascript/flavours/glitch/styles/components/columns.scss b/app/javascript/flavours/glitch/styles/components/columns.scss
index 71edf7fb3..42a591931 100644
--- a/app/javascript/flavours/glitch/styles/components/columns.scss
+++ b/app/javascript/flavours/glitch/styles/components/columns.scss
@@ -65,6 +65,7 @@ $ui-header-height: 55px;
   z-index: 2;
   justify-content: space-between;
   align-items: center;
+  overflow: hidden;
 
   &__logo {
     display: inline-flex;
@@ -81,10 +82,15 @@ $ui-header-height: 55px;
     align-items: center;
     gap: 10px;
     padding: 0 10px;
+    overflow: hidden;
 
     .button {
       flex: 0 0 auto;
     }
+
+    .button-tertiary {
+      flex-shrink: 1;
+    }
   }
 }
 
diff --git a/app/javascript/flavours/glitch/styles/components/drawer.scss b/app/javascript/flavours/glitch/styles/components/drawer.scss
index dfb9dc595..3e2604d4d 100644
--- a/app/javascript/flavours/glitch/styles/components/drawer.scss
+++ b/app/javascript/flavours/glitch/styles/components/drawer.scss
@@ -92,7 +92,7 @@
   @include search-popout();
 }
 
-.drawer--account {
+.navigation-bar {
   padding: 10px;
   color: $darker-text-color;
   display: flex;
diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss
index 80b0598a5..84aca2ebc 100644
--- a/app/javascript/flavours/glitch/styles/components/index.scss
+++ b/app/javascript/flavours/glitch/styles/components/index.scss
@@ -15,7 +15,7 @@
   display: block;
   font-size: 15px;
   line-height: 20px;
-  color: $ui-highlight-color;
+  color: $highlight-text-color;
   border: 0;
   background: transparent;
   padding: 0;
diff --git a/app/javascript/flavours/glitch/styles/components/modal.scss b/app/javascript/flavours/glitch/styles/components/modal.scss
index ab8609170..8ba8bec10 100644
--- a/app/javascript/flavours/glitch/styles/components/modal.scss
+++ b/app/javascript/flavours/glitch/styles/components/modal.scss
@@ -1306,7 +1306,8 @@ img.modal-warning {
   width: 600px;
   background: $ui-base-color;
   border-radius: 8px;
-  overflow: hidden;
+  overflow-x: hidden;
+  overflow-y: auto;
   position: relative;
   display: block;
   padding: 20px;
diff --git a/app/javascript/flavours/glitch/styles/components/single_column.scss b/app/javascript/flavours/glitch/styles/components/single_column.scss
index d91306151..45d57aedd 100644
--- a/app/javascript/flavours/glitch/styles/components/single_column.scss
+++ b/app/javascript/flavours/glitch/styles/components/single_column.scss
@@ -37,7 +37,7 @@
     top: 15px;
   }
 
-  .drawer--account {
+  .navigation-bar {
     flex: 0 1 48px;
   }
 
diff --git a/app/javascript/flavours/glitch/styles/components/status.scss b/app/javascript/flavours/glitch/styles/components/status.scss
index 5e4bddc67..64dbc3cf0 100644
--- a/app/javascript/flavours/glitch/styles/components/status.scss
+++ b/app/javascript/flavours/glitch/styles/components/status.scss
@@ -206,15 +206,13 @@
   }
 }
 
-.status__content__edited-label {
-  display: block;
-  cursor: default;
+.translate-button {
+  margin-top: 16px;
   font-size: 15px;
   line-height: 20px;
-  padding: 0;
-  padding-top: 8px;
+  display: flex;
+  justify-content: space-between;
   color: $dark-text-color;
-  font-weight: 500;
 }
 
 .status__content__spoiler-link {
diff --git a/app/javascript/flavours/glitch/styles/statuses.scss b/app/javascript/flavours/glitch/styles/statuses.scss
index 947a5d3ae..88fa3ffa0 100644
--- a/app/javascript/flavours/glitch/styles/statuses.scss
+++ b/app/javascript/flavours/glitch/styles/statuses.scss
@@ -268,7 +268,7 @@ a.button.logo-button {
   border: 0;
   background: transparent;
   padding: 0;
-  padding-top: 8px;
+  padding-top: 16px;
   text-decoration: none;
 
   &:hover,
diff --git a/app/javascript/flavours/vanilla/names.yml b/app/javascript/flavours/vanilla/names.yml
index 0e3f43ed0..9b7fc189d 100644
--- a/app/javascript/flavours/vanilla/names.yml
+++ b/app/javascript/flavours/vanilla/names.yml
@@ -6,6 +6,14 @@ en:
   skins:
     vanilla:
       default: Default
+cs:
+  flavours:
+    vanilla:
+      description: Standardní rozhraní Mastodonu. Některé funkce GlitchSoc v něm nejsou podporované.
+      name: Standardní Mastodon
+  skins:
+    vanilla:
+      default: Výchozí
 pl:
   flavours:
     vanilla: