diff --git a/Gemfile b/Gemfile index b9bdd82403..2505194c87 100644 --- a/Gemfile +++ b/Gemfile @@ -116,9 +116,9 @@ group :production, :test do end group :test do - gem 'capybara', '~> 3.28' + gem 'capybara', '~> 3.29' gem 'climate_control', '~> 0.2' - gem 'faker', '~> 2.2' + gem 'faker', '~> 2.3' gem 'microformats', '~> 4.1' gem 'rails-controller-testing', '~> 1.0' gem 'rspec-sidekiq', '~> 3.0' diff --git a/Gemfile.lock b/Gemfile.lock index 30e5fc1ff8..092f691199 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -150,7 +150,7 @@ GEM sshkit (~> 1.3) capistrano-yarn (2.0.2) capistrano (~> 3.0) - capybara (3.28.0) + capybara (3.29.0) addressable mini_mime (>= 0.1.3) nokogiri (~> 1.8) @@ -231,7 +231,7 @@ GEM tzinfo excon (0.62.0) fabrication (2.20.2) - faker (2.2.2) + faker (2.3.0) i18n (~> 1.6.0) faraday (0.15.0) multipart-post (>= 1.2, < 3) @@ -323,7 +323,7 @@ GEM multi_json (~> 1.12) rdf (~> 3.0) jsonapi-renderer (0.2.2) - jwt (2.2.1) + jwt (2.1.0) kaminari (1.1.1) activesupport (>= 4.1.0) kaminari-actionview (= 1.1.1) @@ -381,7 +381,7 @@ GEM net-scp (2.0.0) net-ssh (>= 2.6.5, < 6.0.0) net-ssh (5.2.0) - nio4r (2.4.0) + nio4r (2.5.1) nokogiri (1.10.4) mini_portile2 (~> 2.4.0) nokogumbo (2.0.1) @@ -447,7 +447,7 @@ GEM pry-rails (0.3.9) pry (>= 0.10.4) public_suffix (4.0.1) - puma (4.1.0) + puma (4.1.1) nio4r (~> 2.0) pundit (2.1.0) activesupport (>= 3.0.0) @@ -640,7 +640,7 @@ GEM unf (~> 0.1.0) tzinfo (1.2.5) thread_safe (~> 0.1) - tzinfo-data (1.2019.2) + tzinfo-data (1.2019.3) tzinfo (>= 1.0.0) unf (0.1.4) unf_ext @@ -649,7 +649,7 @@ GEM uniform_notifier (1.12.1) warden (1.2.8) rack (>= 2.0.6) - webmock (3.7.1) + webmock (3.7.3) addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) @@ -657,7 +657,7 @@ GEM activesupport (>= 4.2) rack-proxy (>= 0.6.1) railties (>= 4.2) - webpush (1.0.0) + webpush (0.3.8) hkdf (~> 0.2) jwt (~> 2.0) websocket-driver (0.7.0) @@ -688,7 +688,7 @@ DEPENDENCIES capistrano-rails (~> 1.4) capistrano-rbenv (~> 2.1) capistrano-yarn (~> 2.0) - capybara (~> 3.28) + capybara (~> 3.29) charlock_holmes (~> 0.7.6) chewy (~> 5.0) cld3 (~> 3.2.4) @@ -703,7 +703,7 @@ DEPENDENCIES doorkeeper (~> 5.1) dotenv-rails (~> 2.7) fabrication (~> 2.20) - faker (~> 2.2) + faker (~> 2.3) fast_blank (~> 1.0) fastimage fog-core (<= 2.1.0) diff --git a/app/controllers/admin/tags_controller.rb b/app/controllers/admin/tags_controller.rb index 376ebe44d3..65341bbfbe 100644 --- a/app/controllers/admin/tags_controller.rb +++ b/app/controllers/admin/tags_controller.rb @@ -2,7 +2,6 @@ module Admin class TagsController < BaseController - before_action :set_tags, only: :index before_action :set_tag, except: [:index, :batch, :approve_all, :reject_all] before_action :set_usage_by_domain, except: [:index, :batch, :approve_all, :reject_all] before_action :set_counters, except: [:index, :batch, :approve_all, :reject_all] @@ -10,6 +9,7 @@ module Admin def index authorize :tag, :index? + @tags = filtered_tags.page(params[:page]) @form = Form::TagBatch.new end @@ -48,10 +48,6 @@ module Admin private - def set_tags - @tags = filtered_tags.page(params[:page]) - end - def set_tag @tag = Tag.find(params[:id]) end @@ -73,16 +69,11 @@ module Admin end def filtered_tags - scope = Tag - scope = scope.discoverable if filter_params[:context] == 'directory' - scope = scope.unreviewed if filter_params[:review] == 'unreviewed' - scope = scope.reviewed.order(reviewed_at: :desc) if filter_params[:review] == 'reviewed' - scope = scope.pending_review.order(requested_review_at: :desc) if filter_params[:review] == 'pending_review' - scope.order(max_score: :desc) + TagFilter.new(filter_params).results end def filter_params - params.slice(:context, :review, :page).permit(:context, :review, :page) + params.slice(:directory, :reviewed, :unreviewed, :pending_review, :page, :popular, :active, :name).permit(:directory, :reviewed, :unreviewed, :pending_review, :page, :popular, :active, :name) end def tag_params diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index 7ecbaf1930..c2b38883ba 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -8,7 +8,6 @@ class Auth::SessionsController < Devise::SessionsController skip_before_action :require_no_authentication, only: [:create] skip_before_action :require_functional! - prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create] prepend_before_action :set_pack before_action :set_instance_presenter, only: [:new] @@ -23,9 +22,22 @@ class Auth::SessionsController < Devise::SessionsController end def create - super do |resource| - remember_me(resource) - flash.delete(:notice) + self.resource = begin + if user_params[:email].blank? && session[:otp_user_id].present? + User.find(session[:otp_user_id]) + else + warden.authenticate!(auth_options) + end + end + + if resource.otp_required_for_login? + if user_params[:otp_attempt].present? && session[:otp_user_id].present? + authenticate_with_two_factor_via_otp(resource) + else + prompt_for_two_factor(resource) + end + else + authenticate_and_respond(resource) end end @@ -38,18 +50,6 @@ class Auth::SessionsController < Devise::SessionsController protected - def find_user - if session[:otp_user_id] - User.find(session[:otp_user_id]) - elsif user_params[:email] - if use_seamless_external_login? && Devise.check_at_sign && user_params[:email].index('@').nil? - User.joins(:account).find_by(accounts: { username: user_params[:email] }) - else - User.find_for_authentication(email: user_params[:email]) - end - end - end - def user_params params.require(:user).permit(:email, :password, :otp_attempt) end @@ -72,32 +72,17 @@ class Auth::SessionsController < Devise::SessionsController super end - def two_factor_enabled? - find_user.try(:otp_required_for_login?) - end - def valid_otp_attempt?(user) user.validate_and_consume_otp!(user_params[:otp_attempt]) || user.invalidate_otp_backup_code!(user_params[:otp_attempt]) - rescue OpenSSL::Cipher::CipherError => _error + rescue OpenSSL::Cipher::CipherError false end - def authenticate_with_two_factor - user = self.resource = find_user - - if user_params[:otp_attempt].present? && session[:otp_user_id] - authenticate_with_two_factor_via_otp(user) - elsif user&.valid_password?(user_params[:password]) - prompt_for_two_factor(user) - end - end - def authenticate_with_two_factor_via_otp(user) if valid_otp_attempt?(user) session.delete(:otp_user_id) - remember_me(user) - sign_in(user) + authenticate_and_respond(user) else flash.now[:alert] = I18n.t('users.invalid_otp_token') prompt_for_two_factor(user) @@ -109,6 +94,13 @@ class Auth::SessionsController < Devise::SessionsController render :two_factor end + def authenticate_and_respond(user) + sign_in(user) + remember_me(user) + + respond_with user, location: after_sign_in_path_for(user) + end + private def set_pack @@ -125,9 +117,11 @@ class Auth::SessionsController < Devise::SessionsController def home_paths(resource) paths = [about_path] + if single_user_mode? && resource.is_a?(User) paths << short_account_path(username: resource.account) end + paths end diff --git a/app/controllers/settings/deletes_controller.rb b/app/controllers/settings/deletes_controller.rb index 97fe4d3281..15a59c999d 100644 --- a/app/controllers/settings/deletes_controller.rb +++ b/app/controllers/settings/deletes_controller.rb @@ -14,12 +14,11 @@ class Settings::DeletesController < Settings::BaseController end def destroy - if current_user.valid_password?(delete_params[:password]) - Admin::SuspensionWorker.perform_async(current_user.account_id, true) - sign_out + if challenge_passed? + destroy_account! redirect_to new_user_session_path, notice: I18n.t('deletes.success_msg') else - redirect_to settings_delete_path, alert: I18n.t('deletes.bad_password_msg') + redirect_to settings_delete_path, alert: I18n.t('deletes.challenge_not_passed') end end @@ -29,11 +28,25 @@ class Settings::DeletesController < Settings::BaseController redirect_to root_path unless Setting.open_deletion end - def delete_params - params.require(:form_delete_confirmation).permit(:password) + def resource_params + params.require(:form_delete_confirmation).permit(:password, :username) end def require_not_suspended! forbidden if current_account.suspended? end + + def challenge_passed? + if current_user.encrypted_password.blank? + current_account.username == resource_params[:username] + else + current_user.valid_password?(resource_params[:password]) + end + end + + def destroy_account! + current_account.suspend! + Admin::SuspensionWorker.perform_async(current_user.account_id, true) + sign_out + end end diff --git a/app/controllers/settings/two_factor_authentication/confirmations_controller.rb b/app/controllers/settings/two_factor_authentication/confirmations_controller.rb index 3145e092da..46c90bf74a 100644 --- a/app/controllers/settings/two_factor_authentication/confirmations_controller.rb +++ b/app/controllers/settings/two_factor_authentication/confirmations_controller.rb @@ -15,7 +15,7 @@ module Settings end def create - if current_user.validate_and_consume_otp!(confirmation_params[:code]) + if current_user.validate_and_consume_otp!(confirmation_params[:otp_attempt]) flash.now[:notice] = I18n.t('two_factor_authentication.enabled_success') current_user.otp_required_for_login = true @@ -33,7 +33,7 @@ module Settings private def confirmation_params - params.require(:form_two_factor_confirmation).permit(:code) + params.require(:form_two_factor_confirmation).permit(:otp_attempt) end def prepare_two_factor_form diff --git a/app/controllers/settings/two_factor_authentications_controller.rb b/app/controllers/settings/two_factor_authentications_controller.rb index 6904076e42..c93b175770 100644 --- a/app/controllers/settings/two_factor_authentications_controller.rb +++ b/app/controllers/settings/two_factor_authentications_controller.rb @@ -34,7 +34,7 @@ module Settings private def confirmation_params - params.require(:form_two_factor_confirmation).permit(:code) + params.require(:form_two_factor_confirmation).permit(:otp_attempt) end def verify_otp_required @@ -42,8 +42,8 @@ module Settings end def acceptable_code? - current_user.validate_and_consume_otp!(confirmation_params[:code]) || - current_user.invalidate_otp_backup_code!(confirmation_params[:code]) + current_user.validate_and_consume_otp!(confirmation_params[:otp_attempt]) || + current_user.invalidate_otp_backup_code!(confirmation_params[:otp_attempt]) end end end diff --git a/app/controllers/well_known/webfinger_controller.rb b/app/controllers/well_known/webfinger_controller.rb index d60bf98ab0..480e58f3f0 100644 --- a/app/controllers/well_known/webfinger_controller.rb +++ b/app/controllers/well_known/webfinger_controller.rb @@ -5,18 +5,22 @@ module WellKnown include RoutingHelper before_action { response.headers['Vary'] = 'Accept' } + before_action :set_account + before_action :check_account_suspension + + rescue_from ActiveRecord::RecordNotFound, ActionController::ParameterMissing, with: :not_found def show - @account = Account.find_local!(username_from_resource) - expires_in 3.days, public: true render json: @account, serializer: WebfingerSerializer, content_type: 'application/jrd+json' - rescue ActiveRecord::RecordNotFound, ActionController::ParameterMissing - head 404 end private + def set_account + @account = Account.find_local!(username_from_resource) + end + def username_from_resource resource_user = resource_param username, domain = resource_user.split('@') @@ -28,5 +32,17 @@ module WellKnown def resource_param params.require(:resource) end + + def check_account_suspension + expires_in(3.minutes, public: true) && gone if @account.suspended? + end + + def not_found + head 404 + end + + def gone + head 410 + end end end diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb index 506429e10b..8af1683e76 100644 --- a/app/helpers/admin/filter_helper.rb +++ b/app/helpers/admin/filter_helper.rb @@ -5,7 +5,7 @@ module Admin::FilterHelper REPORT_FILTERS = %i(resolved account_id target_account_id).freeze INVITE_FILTER = %i(available expired).freeze CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze - TAGS_FILTERS = %i(context review).freeze + TAGS_FILTERS = %i(directory reviewed unreviewed pending_review popular active name).freeze INSTANCES_FILTERS = %i(limited by_domain).freeze FOLLOWERS_FILTERS = %i(relationship status by_domain activity order).freeze diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 0cfde7edc6..2b3fd1263a 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -42,7 +42,8 @@ module SettingsHelper no: 'Norsk', oc: 'Occitan', pl: 'Polski', - pt: 'Português (Portugal)', + pt: 'Português', + 'pt-PT': 'Português (Portugal)', 'pt-BR': 'Português (Brasil)', ro: 'Română', ru: 'Русский', diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index 0c23313747..be48b1c77c 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -165,7 +165,7 @@ export function expandNotifications({ maxId } = {}, done = noOp) { dispatch(importFetchedAccounts(response.data.map(item => item.account))); dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status))); - dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent && preferPendingItems)); + dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems)); fetchRelatedRelationships(dispatch, response.data); done(); }).catch(error => { @@ -182,11 +182,12 @@ export function expandNotificationsRequest(isLoadingMore) { }; }; -export function expandNotificationsSuccess(notifications, next, isLoadingMore, usePendingItems) { +export function expandNotificationsSuccess(notifications, next, isLoadingMore, isLoadingRecent, usePendingItems) { return { type: NOTIFICATIONS_EXPAND_SUCCESS, notifications, next, + isLoadingRecent: isLoadingRecent, usePendingItems, skipLoading: !isLoadingMore, }; diff --git a/app/javascript/flavours/glitch/components/poll.js b/app/javascript/flavours/glitch/components/poll.js index 36c4b236c8..905aa54c13 100644 --- a/app/javascript/flavours/glitch/components/poll.js +++ b/app/javascript/flavours/glitch/components/poll.js @@ -32,8 +32,38 @@ class Poll extends ImmutablePureComponent { state = { selected: {}, + expired: null, }; + static getDerivedStateFromProps (props, state) { + const { poll, intl } = props; + const expired = poll.get('expired') || (new Date(poll.get('expires_at'))).getTime() < intl.now(); + return (expired === state.expired) ? null : { expired }; + } + + componentDidMount () { + this._setupTimer(); + } + + componentDidUpdate () { + this._setupTimer(); + } + + componentWillUnmount () { + clearTimeout(this._timer); + } + + _setupTimer () { + const { poll, intl } = this.props; + clearTimeout(this._timer); + if (!this.state.expired) { + const delay = (new Date(poll.get('expires_at'))).getTime() - intl.now(); + this._timer = setTimeout(() => { + this.setState({ expired: true }); + }, delay); + } + } + handleOptionChange = e => { const { target: { value } } = e; @@ -68,12 +98,11 @@ class Poll extends ImmutablePureComponent { this.props.dispatch(fetchPoll(this.props.poll.get('id'))); }; - renderOption (option, optionIndex) { + renderOption (option, optionIndex, showResults) { const { poll, disabled } = this.props; const percent = poll.get('votes_count') === 0 ? 0 : (option.get('votes_count') / poll.get('votes_count')) * 100; const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count')); const active = !!this.state.selected[`${optionIndex}`]; - const showResults = poll.get('voted') || poll.get('expired'); let titleEmojified = option.get('title_emojified'); if (!titleEmojified) { @@ -112,19 +141,20 @@ class Poll extends ImmutablePureComponent { render () { const { poll, intl } = this.props; + const { expired } = this.state; if (!poll) { return null; } - const timeRemaining = poll.get('expired') ? intl.formatMessage(messages.closed) : ; - const showResults = poll.get('voted') || poll.get('expired'); + const timeRemaining = expired ? intl.formatMessage(messages.closed) : ; + const showResults = poll.get('voted') || expired; const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item); return (
    - {poll.get('options').map((option, i) => this.renderOption(option, i))} + {poll.get('options').map((option, i) => this.renderOption(option, i, showResults))}
diff --git a/app/javascript/flavours/glitch/components/scrollable_list.js b/app/javascript/flavours/glitch/components/scrollable_list.js index 5f42bdd8b3..7c0b6d0829 100644 --- a/app/javascript/flavours/glitch/components/scrollable_list.js +++ b/app/javascript/flavours/glitch/components/scrollable_list.js @@ -153,7 +153,9 @@ export default class ScrollableList extends PureComponent { const someItemInserted = React.Children.count(prevProps.children) > 0 && React.Children.count(prevProps.children) < React.Children.count(this.props.children) && this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props); - if (someItemInserted && (this.node.scrollTop > 0 || this.mouseMovedRecently)) { + const pendingChanged = (prevProps.numPending > 0) !== (this.props.numPending > 0); + + if (pendingChanged || someItemInserted && (this.node.scrollTop > 0 || this.mouseMovedRecently)) { return this.node.scrollHeight - this.node.scrollTop; } else { return null; @@ -228,6 +230,13 @@ export default class ScrollableList extends PureComponent { handleLoadPending = e => { e.preventDefault(); this.props.onLoadPending(); + // Prevent the weird scroll-jumping behavior, as we explicitly don't want to + // scroll to top, and we know the scroll height is going to change + this.scrollToTopOnMouseIdle = false; + this.lastScrollWasSynthetic = false; + this.clearMouseIdleTimer(); + this.mouseIdleTimer = setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY); + this.mouseMovedRecently = true; } render () { diff --git a/app/javascript/flavours/glitch/features/community_timeline/index.js b/app/javascript/flavours/glitch/features/community_timeline/index.js index cb7d72c534..5585edc9c3 100644 --- a/app/javascript/flavours/glitch/features/community_timeline/index.js +++ b/app/javascript/flavours/glitch/features/community_timeline/index.js @@ -18,9 +18,10 @@ const mapStateToProps = (state, { onlyMedia, columnId }) => { const uuid = columnId; const columns = state.getIn(['settings', 'columns']); const index = columns.findIndex(c => c.get('uuid') === uuid); + const timelineState = state.getIn(['timelines', `community${onlyMedia ? ':media' : ''}`]); return { - hasUnread: state.getIn(['timelines', `community${onlyMedia ? ':media' : ''}`, 'unread']) > 0, + hasUnread: !!timelineState && (timelineState.get('unread') > 0 || timelineState.get('pendingItems').size > 0), onlyMedia: (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'community', 'other', 'onlyMedia']), }; }; diff --git a/app/javascript/flavours/glitch/features/notifications/index.js b/app/javascript/flavours/glitch/features/notifications/index.js index bd1af97a99..99b2391c7c 100644 --- a/app/javascript/flavours/glitch/features/notifications/index.js +++ b/app/javascript/flavours/glitch/features/notifications/index.js @@ -47,7 +47,7 @@ const mapStateToProps = state => ({ notifications: getNotifications(state), localSettings: state.get('local_settings'), isLoading: state.getIn(['notifications', 'isLoading'], true), - isUnread: state.getIn(['notifications', 'unread']) > 0, + isUnread: state.getIn(['notifications', 'unread']) > 0 || state.getIn(['notifications', 'pendingItems']).size > 0, hasMore: state.getIn(['notifications', 'hasMore']), numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size, notifCleaningActive: state.getIn(['notifications', 'cleaningMode']), diff --git a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js index ced7bd238d..8bded391a8 100644 --- a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js @@ -226,7 +226,7 @@ class FocalPointModal extends ImmutablePureComponent {
-
diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js index 7d9aeb02a8..6cc1b1cde2 100644 --- a/app/javascript/flavours/glitch/features/ui/index.js +++ b/app/javascript/flavours/glitch/features/ui/index.js @@ -64,6 +64,7 @@ const messages = defineMessages({ const mapStateToProps = state => ({ hasComposingText: state.getIn(['compose', 'text']).trim().length !== 0, hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0, + canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < 4, layout: state.getIn(['local_settings', 'layout']), isWide: state.getIn(['local_settings', 'stretch']), navbarUnder: state.getIn(['local_settings', 'navbar_under']), @@ -230,6 +231,7 @@ class UI extends React.Component { isComposing: PropTypes.bool, hasComposingText: PropTypes.bool, hasMediaAttachments: PropTypes.bool, + canUploadMore: PropTypes.bool, match: PropTypes.object.isRequired, location: PropTypes.object.isRequired, history: PropTypes.object.isRequired, @@ -272,7 +274,7 @@ class UI extends React.Component { this.dragTargets.push(e.target); } - if (e.dataTransfer && e.dataTransfer.types.includes('Files')) { + if (e.dataTransfer && e.dataTransfer.types.includes('Files') && this.props.canUploadMore) { this.setState({ draggingOver: true }); } } @@ -293,12 +295,13 @@ class UI extends React.Component { handleDrop = (e) => { if (this.dataTransferIsText(e.dataTransfer)) return; + e.preventDefault(); this.setState({ draggingOver: false }); this.dragTargets = []; - if (e.dataTransfer && e.dataTransfer.files.length >= 1) { + if (e.dataTransfer && e.dataTransfer.files.length >= 1 && this.props.canUploadMore) { this.props.dispatch(uploadCompose(e.dataTransfer.files)); } } diff --git a/app/javascript/flavours/glitch/locales/pt.js b/app/javascript/flavours/glitch/locales/pt-PT.js similarity index 67% rename from app/javascript/flavours/glitch/locales/pt.js rename to app/javascript/flavours/glitch/locales/pt-PT.js index 0156f55ff2..cf7afd17ac 100644 --- a/app/javascript/flavours/glitch/locales/pt.js +++ b/app/javascript/flavours/glitch/locales/pt-PT.js @@ -1,4 +1,4 @@ -import inherited from 'mastodon/locales/pt.json'; +import inherited from 'mastodon/locales/pt-PT.json'; const messages = { // No translations available. diff --git a/app/javascript/flavours/glitch/packs/public.js b/app/javascript/flavours/glitch/packs/public.js index 72725d20bc..019de2167e 100644 --- a/app/javascript/flavours/glitch/packs/public.js +++ b/app/javascript/flavours/glitch/packs/public.js @@ -11,10 +11,10 @@ function main() { const React = require('react'); const ReactDOM = require('react-dom'); const Rellax = require('rellax'); - const createHistory = require('history').createBrowserHistory; + const { createBrowserHistory } = require('history'); const scrollToDetailedStatus = () => { - const history = createHistory(); + const history = createBrowserHistory(); const detailedStatuses = document.querySelectorAll('.public-layout .detailed-status'); const location = history.location; diff --git a/app/javascript/flavours/glitch/reducers/notifications.js b/app/javascript/flavours/glitch/reducers/notifications.js index 135995da66..8dc7a4aba3 100644 --- a/app/javascript/flavours/glitch/reducers/notifications.js +++ b/app/javascript/flavours/glitch/reducers/notifications.js @@ -50,12 +50,12 @@ const notificationToMap = (state, notification) => ImmutableMap({ }); const normalizeNotification = (state, notification, usePendingItems) => { - if (usePendingItems) { - return state.update('pendingItems', list => list.unshift(notificationToMap(state, notification))); - } - const top = !shouldCountUnreadNotifications(state); + if (usePendingItems || !top || !state.get('pendingItems').isEmpty()) { + return state.update('pendingItems', list => list.unshift(notificationToMap(state, notification))).update('unread', unread => unread + 1); + } + if (top) { state = state.set('lastReadId', notification.id); } else { @@ -71,7 +71,7 @@ const normalizeNotification = (state, notification, usePendingItems) => { }); }; -const expandNormalizedNotifications = (state, notifications, next, usePendingItems) => { +const expandNormalizedNotifications = (state, notifications, next, isLoadingRecent, usePendingItems) => { const top = !(shouldCountUnreadNotifications(state)); const lastReadId = state.get('lastReadId'); let items = ImmutableList(); @@ -82,6 +82,8 @@ const expandNormalizedNotifications = (state, notifications, next, usePendingIte return state.withMutations(mutable => { if (!items.isEmpty()) { + usePendingItems = isLoadingRecent && (usePendingItems || !mutable.get('top') || !mutable.get('pendingItems').isEmpty()); + mutable.update(usePendingItems ? 'pendingItems' : 'items', list => { const lastIndex = 1 + list.findLastIndex( item => item !== null && (compareId(item.get('id'), items.last().get('id')) > 0 || item.get('id') === items.last().get('id')) @@ -117,7 +119,7 @@ const filterNotifications = (state, accountIds) => { }; const clearUnread = (state) => { - state = state.set('unread', 0); + state = state.set('unread', state.get('pendingItems').size); const lastNotification = state.get('items').find(item => item !== null); return state.set('lastReadId', lastNotification ? lastNotification.get('id') : '0'); } @@ -140,6 +142,8 @@ const deleteByStatus = (state, statusId) => { state = state.update('unread', unread => unread - deletedUnread.size); } const helper = list => list.filterNot(item => item !== null && item.get('status') === statusId); + const deletedUnread = state.get('pendingItems').filter(item => item !== null && item.get('status') === statusId && compareId(item.get('id'), lastReadId) > 0); + state = state.update('unread', unread => unread - deletedUnread.size); return state.update('items', helper).update('pendingItems', helper); }; @@ -216,7 +220,7 @@ export default function notifications(state = initialState, action) { case NOTIFICATIONS_UPDATE: return normalizeNotification(state, action.notification, action.usePendingItems); case NOTIFICATIONS_EXPAND_SUCCESS: - return expandNormalizedNotifications(state, action.notifications, action.next, action.usePendingItems); + return expandNormalizedNotifications(state, action.notifications, action.next, action.isLoadingRecent, action.usePendingItems); case ACCOUNT_BLOCK_SUCCESS: return filterNotifications(state, [action.relationship.id]); case ACCOUNT_MUTE_SUCCESS: diff --git a/app/javascript/flavours/glitch/reducers/timelines.js b/app/javascript/flavours/glitch/reducers/timelines.js index 9b016a4c6d..e6bef18e99 100644 --- a/app/javascript/flavours/glitch/reducers/timelines.js +++ b/app/javascript/flavours/glitch/reducers/timelines.js @@ -40,6 +40,7 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is if (timeline.endsWith(':pinned')) { mMap.set('items', statuses.map(status => status.get('id'))); } else if (!statuses.isEmpty()) { + usePendingItems = isLoadingRecent && (usePendingItems || !mMap.get('top') || !mMap.get('pendingItems').isEmpty()); mMap.update(usePendingItems ? 'pendingItems' : 'items', ImmutableList(), oldIds => { const newIds = statuses.map(status => status.get('id')); const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1; @@ -59,15 +60,16 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is }; const updateTimeline = (state, timeline, status, usePendingItems) => { - if (usePendingItems) { + const top = state.getIn([timeline, 'top']); + + if (usePendingItems || !top || !state.getIn([timeline, 'pendingItems']).isEmpty()) { if (state.getIn([timeline, 'pendingItems'], ImmutableList()).includes(status.get('id')) || state.getIn([timeline, 'items'], ImmutableList()).includes(status.get('id'))) { return state; } - return state.update(timeline, initialTimeline, map => map.update('pendingItems', list => list.unshift(status.get('id')))); + return state.update(timeline, initialTimeline, map => map.update('pendingItems', list => list.unshift(status.get('id'))).update('unread', unread => unread + 1)); } - const top = state.getIn([timeline, 'top']); const ids = state.getIn([timeline, 'items'], ImmutableList()); const includesId = ids.includes(status.get('id')); const unread = state.getIn([timeline, 'unread'], 0); @@ -127,7 +129,7 @@ const filterTimeline = (timeline, state, relationship, statuses) => { const updateTop = (state, timeline, top) => { return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { - if (top) mMap.set('unread', 0); + if (top) mMap.set('unread', mMap.get('pendingItems').size); mMap.set('top', top); })); }; diff --git a/app/javascript/flavours/glitch/styles/accounts.scss b/app/javascript/flavours/glitch/styles/accounts.scss index 0fae137f06..a827d271a8 100644 --- a/app/javascript/flavours/glitch/styles/accounts.scss +++ b/app/javascript/flavours/glitch/styles/accounts.scss @@ -226,6 +226,7 @@ } .account__header__fields { + max-width: 100vw; padding: 0; margin: 15px -15px -15px; border: 0 none; diff --git a/app/javascript/flavours/glitch/styles/polls.scss b/app/javascript/flavours/glitch/styles/polls.scss index 6051ef45db..06f60408dc 100644 --- a/app/javascript/flavours/glitch/styles/polls.scss +++ b/app/javascript/flavours/glitch/styles/polls.scss @@ -86,6 +86,9 @@ top: -1px; border-radius: 50%; vertical-align: middle; + margin-top: auto; + margin-bottom: auto; + flex: 0 0 18px; &.checkbox { border-radius: 4px; diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index d92d972bc7..ea76255e38 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -151,7 +151,7 @@ export function expandNotifications({ maxId } = {}, done = noOp) { dispatch(importFetchedAccounts(response.data.map(item => item.account))); dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status))); - dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent && preferPendingItems)); + dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems)); fetchRelatedRelationships(dispatch, response.data); done(); }).catch(error => { @@ -168,11 +168,12 @@ export function expandNotificationsRequest(isLoadingMore) { }; }; -export function expandNotificationsSuccess(notifications, next, isLoadingMore, usePendingItems) { +export function expandNotificationsSuccess(notifications, next, isLoadingMore, isLoadingRecent, usePendingItems) { return { type: NOTIFICATIONS_EXPAND_SUCCESS, notifications, next, + isLoadingRecent: isLoadingRecent, usePendingItems, skipLoading: !isLoadingMore, }; diff --git a/app/javascript/mastodon/components/poll.js b/app/javascript/mastodon/components/poll.js index 690f9ae5a0..373f710d3c 100644 --- a/app/javascript/mastodon/components/poll.js +++ b/app/javascript/mastodon/components/poll.js @@ -32,8 +32,38 @@ class Poll extends ImmutablePureComponent { state = { selected: {}, + expired: null, }; + static getDerivedStateFromProps (props, state) { + const { poll, intl } = props; + const expired = poll.get('expired') || (new Date(poll.get('expires_at'))).getTime() < intl.now(); + return (expired === state.expired) ? null : { expired }; + } + + componentDidMount () { + this._setupTimer(); + } + + componentDidUpdate () { + this._setupTimer(); + } + + componentWillUnmount () { + clearTimeout(this._timer); + } + + _setupTimer () { + const { poll, intl } = this.props; + clearTimeout(this._timer); + if (!this.state.expired) { + const delay = (new Date(poll.get('expires_at'))).getTime() - intl.now(); + this._timer = setTimeout(() => { + this.setState({ expired: true }); + }, delay); + } + } + handleOptionChange = e => { const { target: { value } } = e; @@ -68,12 +98,11 @@ class Poll extends ImmutablePureComponent { this.props.dispatch(fetchPoll(this.props.poll.get('id'))); }; - renderOption (option, optionIndex) { + renderOption (option, optionIndex, showResults) { const { poll, disabled } = this.props; const percent = poll.get('votes_count') === 0 ? 0 : (option.get('votes_count') / poll.get('votes_count')) * 100; const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count')); const active = !!this.state.selected[`${optionIndex}`]; - const showResults = poll.get('voted') || poll.get('expired'); let titleEmojified = option.get('title_emojified'); if (!titleEmojified) { @@ -112,19 +141,20 @@ class Poll extends ImmutablePureComponent { render () { const { poll, intl } = this.props; + const { expired } = this.state; if (!poll) { return null; } - const timeRemaining = poll.get('expired') ? intl.formatMessage(messages.closed) : ; - const showResults = poll.get('voted') || poll.get('expired'); + const timeRemaining = expired ? intl.formatMessage(messages.closed) : ; + const showResults = poll.get('voted') || expired; const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item); return (
    - {poll.get('options').map((option, i) => this.renderOption(option, i))} + {poll.get('options').map((option, i) => this.renderOption(option, i, showResults))}
diff --git a/app/javascript/mastodon/components/scrollable_list.js b/app/javascript/mastodon/components/scrollable_list.js index 0bf8179233..253646ed01 100644 --- a/app/javascript/mastodon/components/scrollable_list.js +++ b/app/javascript/mastodon/components/scrollable_list.js @@ -172,8 +172,9 @@ export default class ScrollableList extends PureComponent { const someItemInserted = React.Children.count(prevProps.children) > 0 && React.Children.count(prevProps.children) < React.Children.count(this.props.children) && this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props); + const pendingChanged = (prevProps.numPending > 0) !== (this.props.numPending > 0); - if (someItemInserted && (this.getScrollTop() > 0 || this.mouseMovedRecently)) { + if (pendingChanged || someItemInserted && (this.getScrollTop() > 0 || this.mouseMovedRecently)) { return this.getScrollHeight() - this.getScrollTop(); } else { return null; @@ -261,6 +262,13 @@ export default class ScrollableList extends PureComponent { handleLoadPending = e => { e.preventDefault(); this.props.onLoadPending(); + // Prevent the weird scroll-jumping behavior, as we explicitly don't want to + // scroll to top, and we know the scroll height is going to change + this.scrollToTopOnMouseIdle = false; + this.lastScrollWasSynthetic = false; + this.clearMouseIdleTimer(); + this.mouseIdleTimer = setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY); + this.mouseMovedRecently = true; } render () { diff --git a/app/javascript/mastodon/features/community_timeline/index.js b/app/javascript/mastodon/features/community_timeline/index.js index f95fa49709..30153cc153 100644 --- a/app/javascript/mastodon/features/community_timeline/index.js +++ b/app/javascript/mastodon/features/community_timeline/index.js @@ -18,9 +18,10 @@ const mapStateToProps = (state, { onlyMedia, columnId }) => { const uuid = columnId; const columns = state.getIn(['settings', 'columns']); const index = columns.findIndex(c => c.get('uuid') === uuid); + const timelineState = state.getIn(['timelines', `community${onlyMedia ? ':media' : ''}`]); return { - hasUnread: state.getIn(['timelines', `community${onlyMedia ? ':media' : ''}`, 'unread']) > 0, + hasUnread: !!timelineState && (timelineState.get('unread') > 0 || timelineState.get('pendingItems').size > 0), onlyMedia: (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'community', 'other', 'onlyMedia']), }; }; diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js index f2b239afed..7e5de0613a 100644 --- a/app/javascript/mastodon/features/notifications/index.js +++ b/app/javascript/mastodon/features/notifications/index.js @@ -39,7 +39,7 @@ const mapStateToProps = state => ({ showFilterBar: state.getIn(['settings', 'notifications', 'quickFilter', 'show']), notifications: getNotifications(state), isLoading: state.getIn(['notifications', 'isLoading'], true), - isUnread: state.getIn(['notifications', 'unread']) > 0, + isUnread: state.getIn(['notifications', 'unread']) > 0 || state.getIn(['notifications', 'pendingItems']).size > 0, hasMore: state.getIn(['notifications', 'hasMore']), numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size, }); diff --git a/app/javascript/mastodon/features/ui/components/focal_point_modal.js b/app/javascript/mastodon/features/ui/components/focal_point_modal.js index d13138a76b..7891d6690f 100644 --- a/app/javascript/mastodon/features/ui/components/focal_point_modal.js +++ b/app/javascript/mastodon/features/ui/components/focal_point_modal.js @@ -226,7 +226,7 @@ class FocalPointModal extends ImmutablePureComponent {
-
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 63c5622b6b..f5e48ed311 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -66,6 +66,7 @@ const mapStateToProps = state => ({ isComposing: state.getIn(['compose', 'is_composing']), hasComposingText: state.getIn(['compose', 'text']).trim().length !== 0, hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0, + canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < 4, dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null, }); @@ -232,6 +233,7 @@ class UI extends React.PureComponent { isComposing: PropTypes.bool, hasComposingText: PropTypes.bool, hasMediaAttachments: PropTypes.bool, + canUploadMore: PropTypes.bool, location: PropTypes.object, intl: PropTypes.object.isRequired, dropdownMenuIsOpen: PropTypes.bool, @@ -278,13 +280,14 @@ class UI extends React.PureComponent { this.dragTargets.push(e.target); } - if (e.dataTransfer && Array.from(e.dataTransfer.types).includes('Files')) { + if (e.dataTransfer && Array.from(e.dataTransfer.types).includes('Files') && this.props.canUploadMore) { this.setState({ draggingOver: true }); } } handleDragOver = (e) => { if (this.dataTransferIsText(e.dataTransfer)) return false; + e.preventDefault(); e.stopPropagation(); @@ -299,12 +302,13 @@ class UI extends React.PureComponent { handleDrop = (e) => { if (this.dataTransferIsText(e.dataTransfer)) return; + e.preventDefault(); this.setState({ draggingOver: false }); this.dragTargets = []; - if (e.dataTransfer && e.dataTransfer.files.length >= 1) { + if (e.dataTransfer && e.dataTransfer.files.length >= 1 && this.props.canUploadMore) { this.props.dispatch(uploadCompose(e.dataTransfer.files)); } } diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json index 6424cd1a79..ce66a5d1c6 100644 --- a/app/javascript/mastodon/locales/ar.json +++ b/app/javascript/mastodon/locales/ar.json @@ -63,6 +63,7 @@ "column.notifications": "الإخطارات", "column.pins": "التبويقات المثبتة", "column.public": "الخيط العام الموحد", + "column.status": "Toot", "column_back_button.label": "العودة", "column_header.hide_settings": "إخفاء الإعدادات", "column_header.moveLeft_settings": "نقل القائمة إلى اليسار", @@ -112,8 +113,8 @@ "confirmations.unfollow.message": "متأكد من أنك تريد إلغاء متابعة {name} ؟", "directory.federated": "From known fediverse", "directory.local": "From {domain} only", - "directory.new_arrivals": "New arrivals", - "directory.recently_active": "Recently active", + "directory.new_arrivals": "الوافدون الجُدد", + "directory.recently_active": "نشط مؤخرا", "embed.instructions": "يمكنكم إدماج هذا المنشور على موقعكم الإلكتروني عن طريق نسخ الشفرة أدناه.", "embed.preview": "هكذا ما سوف يبدو عليه:", "emoji_button.activity": "الأنشطة", @@ -368,7 +369,7 @@ "status.show_more": "أظهر المزيد", "status.show_more_all": "توسيع الكل", "status.show_thread": "الكشف عن المحادثة", - "status.uncached_media_warning": "Not available", + "status.uncached_media_warning": "غير متوفر", "status.unmute_conversation": "فك الكتم عن المحادثة", "status.unpin": "فك التدبيس من الملف الشخصي", "suggestions.dismiss": "إلغاء الاقتراح", diff --git a/app/javascript/mastodon/locales/ast.json b/app/javascript/mastodon/locales/ast.json index ef17d6d64f..2ef693fcbc 100644 --- a/app/javascript/mastodon/locales/ast.json +++ b/app/javascript/mastodon/locales/ast.json @@ -63,6 +63,7 @@ "column.notifications": "Avisos", "column.pins": "Toots fixaos", "column.public": "Llinia temporal federada", + "column.status": "Toot", "column_back_button.label": "Atrás", "column_header.hide_settings": "Hide settings", "column_header.moveLeft_settings": "Mover la columna a la esquierda", diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json index b0954f1990..309f045132 100644 --- a/app/javascript/mastodon/locales/bg.json +++ b/app/javascript/mastodon/locales/bg.json @@ -63,6 +63,7 @@ "column.notifications": "Известия", "column.pins": "Pinned toot", "column.public": "Публичен канал", + "column.status": "Toot", "column_back_button.label": "Назад", "column_header.hide_settings": "Hide settings", "column_header.moveLeft_settings": "Move column to the left", diff --git a/app/javascript/mastodon/locales/bn.json b/app/javascript/mastodon/locales/bn.json index 241b43573a..e4984a1188 100644 --- a/app/javascript/mastodon/locales/bn.json +++ b/app/javascript/mastodon/locales/bn.json @@ -63,6 +63,7 @@ "column.notifications": "প্রজ্ঞাপনগুলো", "column.pins": "পিন করা টুট", "column.public": "যুক্ত সময়রেখা", + "column.status": "Toot", "column_back_button.label": "পেছনে", "column_header.hide_settings": "সেটিংগুলো সরান", "column_header.moveLeft_settings": "কলমটা বামে সরান", diff --git a/app/javascript/mastodon/locales/br.json b/app/javascript/mastodon/locales/br.json new file mode 100644 index 0000000000..2de037f16c --- /dev/null +++ b/app/javascript/mastodon/locales/br.json @@ -0,0 +1,414 @@ +{ + "account.add_or_remove_from_list": "Add or Remove from lists", + "account.badges.bot": "Bot", + "account.block": "Block @{name}", + "account.block_domain": "Hide everything from {domain}", + "account.blocked": "Blocked", + "account.cancel_follow_request": "Cancel follow request", + "account.direct": "Direct message @{name}", + "account.domain_blocked": "Domain hidden", + "account.edit_profile": "Edit profile", + "account.endorse": "Feature on profile", + "account.follow": "Follow", + "account.followers": "Followers", + "account.followers.empty": "No one follows this user yet.", + "account.follows": "Follows", + "account.follows.empty": "This user doesn't follow anyone yet.", + "account.follows_you": "Follows you", + "account.hide_reblogs": "Hide boosts from @{name}", + "account.last_status": "Last active", + "account.link_verified_on": "Ownership of this link was checked on {date}", + "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", + "account.media": "Media", + "account.mention": "Mention @{name}", + "account.moved_to": "{name} has moved to:", + "account.mute": "Mute @{name}", + "account.mute_notifications": "Mute notifications from @{name}", + "account.muted": "Muted", + "account.never_active": "Never", + "account.posts": "Toots", + "account.posts_with_replies": "Toots and replies", + "account.report": "Report @{name}", + "account.requested": "Awaiting approval", + "account.share": "Share @{name}'s profile", + "account.show_reblogs": "Show boosts from @{name}", + "account.unblock": "Unblock @{name}", + "account.unblock_domain": "Unhide {domain}", + "account.unendorse": "Don't feature on profile", + "account.unfollow": "Unfollow", + "account.unmute": "Unmute @{name}", + "account.unmute_notifications": "Unmute notifications from @{name}", + "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", + "alert.rate_limited.title": "Rate limited", + "alert.unexpected.message": "An unexpected error occurred.", + "alert.unexpected.title": "Oops!", + "autosuggest_hashtag.per_week": "{count} per week", + "boost_modal.combo": "You can press {combo} to skip this next time", + "bundle_column_error.body": "Something went wrong while loading this component.", + "bundle_column_error.retry": "Try again", + "bundle_column_error.title": "Network error", + "bundle_modal_error.close": "Close", + "bundle_modal_error.message": "Something went wrong while loading this component.", + "bundle_modal_error.retry": "Try again", + "column.blocks": "Blocked users", + "column.community": "Local timeline", + "column.direct": "Direct messages", + "column.directory": "Browse profiles", + "column.domain_blocks": "Hidden domains", + "column.favourites": "Favourites", + "column.follow_requests": "Follow requests", + "column.home": "Home", + "column.lists": "Lists", + "column.mutes": "Muted users", + "column.notifications": "Notifications", + "column.pins": "Pinned toot", + "column.public": "Federated timeline", + "column.status": "Toot", + "column_back_button.label": "Back", + "column_header.hide_settings": "Hide settings", + "column_header.moveLeft_settings": "Move column to the left", + "column_header.moveRight_settings": "Move column to the right", + "column_header.pin": "Pin", + "column_header.show_settings": "Show settings", + "column_header.unpin": "Unpin", + "column_subheading.settings": "Settings", + "community.column_settings.media_only": "Media only", + "compose_form.direct_message_warning": "This toot will only be sent to all the mentioned users.", + "compose_form.direct_message_warning_learn_more": "Learn more", + "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.lock_disclaimer.lock": "locked", + "compose_form.placeholder": "What is on your mind?", + "compose_form.poll.add_option": "Add a choice", + "compose_form.poll.duration": "Poll duration", + "compose_form.poll.option_placeholder": "Choice {number}", + "compose_form.poll.remove_option": "Remove this choice", + "compose_form.publish": "Toot", + "compose_form.publish_loud": "{publish}!", + "compose_form.sensitive.hide": "Mark media as sensitive", + "compose_form.sensitive.marked": "Media is marked as sensitive", + "compose_form.sensitive.unmarked": "Media is not marked as sensitive", + "compose_form.spoiler.marked": "Text is hidden behind warning", + "compose_form.spoiler.unmarked": "Text is not hidden", + "compose_form.spoiler_placeholder": "Write your warning here", + "confirmation_modal.cancel": "Cancel", + "confirmations.block.block_and_report": "Block & Report", + "confirmations.block.confirm": "Block", + "confirmations.block.message": "Are you sure you want to block {name}?", + "confirmations.delete.confirm": "Delete", + "confirmations.delete.message": "Are you sure you want to delete this status?", + "confirmations.delete_list.confirm": "Delete", + "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", + "confirmations.domain_block.confirm": "Hide entire domain", + "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.", + "confirmations.logout.confirm": "Log out", + "confirmations.logout.message": "Are you sure you want to log out?", + "confirmations.mute.confirm": "Mute", + "confirmations.mute.message": "Are you sure you want to mute {name}?", + "confirmations.redraft.confirm": "Delete & redraft", + "confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.", + "confirmations.reply.confirm": "Reply", + "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", + "confirmations.unfollow.confirm": "Unfollow", + "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", + "directory.federated": "From known fediverse", + "directory.local": "From {domain} only", + "directory.new_arrivals": "New arrivals", + "directory.recently_active": "Recently active", + "embed.instructions": "Embed this status on your website by copying the code below.", + "embed.preview": "Here is what it will look like:", + "emoji_button.activity": "Activity", + "emoji_button.custom": "Custom", + "emoji_button.flags": "Flags", + "emoji_button.food": "Food & Drink", + "emoji_button.label": "Insert emoji", + "emoji_button.nature": "Nature", + "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻", + "emoji_button.objects": "Objects", + "emoji_button.people": "People", + "emoji_button.recent": "Frequently used", + "emoji_button.search": "Search...", + "emoji_button.search_results": "Search results", + "emoji_button.symbols": "Symbols", + "emoji_button.travel": "Travel & Places", + "empty_column.account_timeline": "No toots here!", + "empty_column.account_unavailable": "Profile unavailable", + "empty_column.blocks": "You haven't blocked any users yet.", + "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", + "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.", + "empty_column.domain_blocks": "There are no hidden domains yet.", + "empty_column.favourited_statuses": "You don't have any favourite toots yet. When you favourite one, it will show up here.", + "empty_column.favourites": "No one has favourited this toot yet. When someone does, they will show up here.", + "empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.", + "empty_column.hashtag": "There is nothing in this hashtag yet.", + "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.", + "empty_column.home.public_timeline": "the public timeline", + "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.", + "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.", + "empty_column.mutes": "You haven't muted any users yet.", + "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.", + "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other servers to fill it up", + "follow_request.authorize": "Authorize", + "follow_request.reject": "Reject", + "getting_started.developers": "Developers", + "getting_started.directory": "Profile directory", + "getting_started.documentation": "Documentation", + "getting_started.heading": "Getting started", + "getting_started.invite": "Invite people", + "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}.", + "getting_started.security": "Security", + "getting_started.terms": "Terms of service", + "hashtag.column_header.tag_mode.all": "and {additional}", + "hashtag.column_header.tag_mode.any": "or {additional}", + "hashtag.column_header.tag_mode.none": "without {additional}", + "hashtag.column_settings.select.no_options_message": "No suggestions found", + "hashtag.column_settings.select.placeholder": "Enter hashtags…", + "hashtag.column_settings.tag_mode.all": "All of these", + "hashtag.column_settings.tag_mode.any": "Any of these", + "hashtag.column_settings.tag_mode.none": "None of these", + "hashtag.column_settings.tag_toggle": "Include additional tags in this column", + "home.column_settings.basic": "Basic", + "home.column_settings.show_reblogs": "Show boosts", + "home.column_settings.show_replies": "Show replies", + "home.column_settings.update_live": "Update in real-time", + "intervals.full.days": "{number, plural, one {# day} other {# days}}", + "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", + "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", + "introduction.federation.action": "Next", + "introduction.federation.federated.headline": "Federated", + "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.", + "introduction.federation.home.headline": "Home", + "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", + "introduction.federation.local.headline": "Local", + "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.", + "introduction.interactions.action": "Finish toot-orial!", + "introduction.interactions.favourite.headline": "Favourite", + "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.", + "introduction.interactions.reblog.headline": "Boost", + "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.", + "introduction.interactions.reply.headline": "Reply", + "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.", + "introduction.welcome.action": "Let's go!", + "introduction.welcome.headline": "First steps", + "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.blocked": "to open blocked users list", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.direct": "to open direct messages column", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.favourites": "to open favourites list", + "keyboard_shortcuts.federated": "to open federated timeline", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.home": "to open home timeline", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.local": "to open local timeline", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.muted": "to open muted users list", + "keyboard_shortcuts.my_profile": "to open your profile", + "keyboard_shortcuts.notifications": "to open notifications column", + "keyboard_shortcuts.pinned": "to open pinned toots list", + "keyboard_shortcuts.profile": "to open author's profile", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.requests": "to open follow requests list", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.start": "to open \"get started\" column", + "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", + "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", + "lightbox.close": "Close", + "lightbox.next": "Next", + "lightbox.previous": "Previous", + "lightbox.view_context": "View context", + "lists.account.add": "Add to list", + "lists.account.remove": "Remove from list", + "lists.delete": "Delete list", + "lists.edit": "Edit list", + "lists.edit.submit": "Change title", + "lists.new.create": "Add list", + "lists.new.title_placeholder": "New list title", + "lists.search": "Search among people you follow", + "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", + "loading_indicator.label": "Loading...", + "media_gallery.toggle_visible": "Toggle visibility", + "missing_indicator.label": "Not found", + "missing_indicator.sublabel": "This resource could not be found", + "mute_modal.hide_notifications": "Hide notifications from this user?", + "navigation_bar.apps": "Mobile apps", + "navigation_bar.blocks": "Blocked users", + "navigation_bar.community_timeline": "Local timeline", + "navigation_bar.compose": "Compose new toot", + "navigation_bar.direct": "Direct messages", + "navigation_bar.discover": "Discover", + "navigation_bar.domain_blocks": "Hidden domains", + "navigation_bar.edit_profile": "Edit profile", + "navigation_bar.favourites": "Favourites", + "navigation_bar.filters": "Muted words", + "navigation_bar.follow_requests": "Follow requests", + "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.info": "About this server", + "navigation_bar.keyboard_shortcuts": "Hotkeys", + "navigation_bar.lists": "Lists", + "navigation_bar.logout": "Logout", + "navigation_bar.mutes": "Muted users", + "navigation_bar.personal": "Personal", + "navigation_bar.pins": "Pinned toots", + "navigation_bar.preferences": "Preferences", + "navigation_bar.public_timeline": "Federated timeline", + "navigation_bar.security": "Security", + "notification.and_n_others": "and {count, plural, one {# other} other {# others}}", + "notification.favourite": "{name} favourited your status", + "notification.follow": "{name} followed you", + "notification.mention": "{name} mentioned you", + "notification.poll": "A poll you have voted in has ended", + "notification.reblog": "{name} boosted your status", + "notifications.clear": "Clear notifications", + "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", + "notifications.column_settings.alert": "Desktop notifications", + "notifications.column_settings.favourite": "Favourites:", + "notifications.column_settings.filter_bar.advanced": "Display all categories", + "notifications.column_settings.filter_bar.category": "Quick filter bar", + "notifications.column_settings.filter_bar.show": "Show", + "notifications.column_settings.follow": "New followers:", + "notifications.column_settings.mention": "Mentions:", + "notifications.column_settings.poll": "Poll results:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.reblog": "Boosts:", + "notifications.column_settings.show": "Show in column", + "notifications.column_settings.sound": "Play sound", + "notifications.filter.all": "All", + "notifications.filter.boosts": "Boosts", + "notifications.filter.favourites": "Favourites", + "notifications.filter.follows": "Follows", + "notifications.filter.mentions": "Mentions", + "notifications.filter.polls": "Poll results", + "notifications.group": "{count} notifications", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", + "poll_button.add_poll": "Add a poll", + "poll_button.remove_poll": "Remove poll", + "privacy.change": "Adjust status privacy", + "privacy.direct.long": "Post to mentioned users only", + "privacy.direct.short": "Direct", + "privacy.private.long": "Post to followers only", + "privacy.private.short": "Followers-only", + "privacy.public.long": "Post to public timelines", + "privacy.public.short": "Public", + "privacy.unlisted.long": "Do not show in public timelines", + "privacy.unlisted.short": "Unlisted", + "regeneration_indicator.label": "Loading…", + "regeneration_indicator.sublabel": "Your home feed is being prepared!", + "relative_time.days": "{number}d", + "relative_time.hours": "{number}h", + "relative_time.just_now": "now", + "relative_time.minutes": "{number}m", + "relative_time.seconds": "{number}s", + "reply_indicator.cancel": "Cancel", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:", + "report.placeholder": "Additional comments", + "report.submit": "Submit", + "report.target": "Report {target}", + "search.placeholder": "Search", + "search_popout.search_format": "Advanced search format", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", + "search_popout.tips.hashtag": "hashtag", + "search_popout.tips.status": "status", + "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", + "search_popout.tips.user": "user", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", + "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "status.admin_account": "Open moderation interface for @{name}", + "status.admin_status": "Open this status in the moderation interface", + "status.block": "Block @{name}", + "status.cancel_reblog_private": "Unboost", + "status.cannot_reblog": "This post cannot be boosted", + "status.copy": "Copy link to status", + "status.delete": "Delete", + "status.detailed_status": "Detailed conversation view", + "status.direct": "Direct message @{name}", + "status.embed": "Embed", + "status.favourite": "Favourite", + "status.filtered": "Filtered", + "status.load_more": "Load more", + "status.media_hidden": "Media hidden", + "status.mention": "Mention @{name}", + "status.more": "More", + "status.mute": "Mute @{name}", + "status.mute_conversation": "Mute conversation", + "status.open": "Expand this status", + "status.pin": "Pin on profile", + "status.pinned": "Pinned toot", + "status.read_more": "Read more", + "status.reblog": "Boost", + "status.reblog_private": "Boost to original audience", + "status.reblogged_by": "{name} boosted", + "status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.", + "status.redraft": "Delete & re-draft", + "status.reply": "Reply", + "status.replyAll": "Reply to thread", + "status.report": "Report @{name}", + "status.sensitive_warning": "Sensitive content", + "status.share": "Share", + "status.show_less": "Show less", + "status.show_less_all": "Show less for all", + "status.show_more": "Show more", + "status.show_more_all": "Show more for all", + "status.show_thread": "Show thread", + "status.uncached_media_warning": "Not available", + "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", + "suggestions.dismiss": "Dismiss suggestion", + "suggestions.header": "You might be interested in…", + "tabs_bar.federated_timeline": "Federated", + "tabs_bar.home": "Home", + "tabs_bar.local_timeline": "Local", + "tabs_bar.notifications": "Notifications", + "tabs_bar.search": "Search", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", + "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "trends.trending_now": "Trending now", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", + "upload_area.title": "Drag & drop to upload", + "upload_button.label": "Add media ({formats})", + "upload_error.limit": "File upload limit exceeded.", + "upload_error.poll": "File upload not allowed with polls.", + "upload_form.description": "Describe for the visually impaired", + "upload_form.edit": "Edit", + "upload_form.undo": "Delete", + "upload_modal.analyzing_picture": "Analyzing picture…", + "upload_modal.apply": "Apply", + "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", + "upload_modal.detect_text": "Detect text from picture", + "upload_modal.edit_media": "Edit media", + "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.", + "upload_modal.preview_label": "Preview ({ratio})", + "upload_progress.label": "Uploading...", + "video.close": "Close video", + "video.exit_fullscreen": "Exit full screen", + "video.expand": "Expand video", + "video.fullscreen": "Full screen", + "video.hide": "Hide video", + "video.mute": "Mute sound", + "video.pause": "Pause", + "video.play": "Play", + "video.unmute": "Unmute sound" +} diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index 4554500f5d..77f84ac7d7 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -63,6 +63,7 @@ "column.notifications": "Notificacions", "column.pins": "Toots fixats", "column.public": "Línia de temps federada", + "column.status": "Toot", "column_back_button.label": "Enrere", "column_header.hide_settings": "Amaga la configuració", "column_header.moveLeft_settings": "Mou la columna cap a l'esquerra", diff --git a/app/javascript/mastodon/locales/co.json b/app/javascript/mastodon/locales/co.json index b54857e369..d95f32b181 100644 --- a/app/javascript/mastodon/locales/co.json +++ b/app/javascript/mastodon/locales/co.json @@ -63,6 +63,7 @@ "column.notifications": "Nutificazione", "column.pins": "Statuti puntarulati", "column.public": "Linea pubblica glubale", + "column.status": "Toot", "column_back_button.label": "Ritornu", "column_header.hide_settings": "Piattà i parametri", "column_header.moveLeft_settings": "Spiazzà à manca", diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json index b3d1e81577..8acf27cb39 100644 --- a/app/javascript/mastodon/locales/cs.json +++ b/app/javascript/mastodon/locales/cs.json @@ -63,6 +63,7 @@ "column.notifications": "Oznámení", "column.pins": "Připnuté tooty", "column.public": "Federovaná časová osa", + "column.status": "Toot", "column_back_button.label": "Zpět", "column_header.hide_settings": "Skrýt nastavení", "column_header.moveLeft_settings": "Posunout sloupec doleva", diff --git a/app/javascript/mastodon/locales/cy.json b/app/javascript/mastodon/locales/cy.json index bc65d601e3..cdf2656d7d 100644 --- a/app/javascript/mastodon/locales/cy.json +++ b/app/javascript/mastodon/locales/cy.json @@ -63,6 +63,7 @@ "column.notifications": "Hysbysiadau", "column.pins": "Tŵtiau wedi eu pinio", "column.public": "Ffrwd y ffederasiwn", + "column.status": "Toot", "column_back_button.label": "Nôl", "column_header.hide_settings": "Cuddio dewisiadau", "column_header.moveLeft_settings": "Symud y golofn i'r chwith", diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index dff8c3c050..14b0f75639 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -63,6 +63,7 @@ "column.notifications": "Notifikationer", "column.pins": "Fastgjorte trut", "column.public": "Fælles tidslinje", + "column.status": "Toot", "column_back_button.label": "Tilbage", "column_header.hide_settings": "Skjul indstillinger", "column_header.moveLeft_settings": "Flyt kolonne til venstre", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index a9b777c035..845bc5156b 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -63,6 +63,7 @@ "column.notifications": "Mitteilungen", "column.pins": "Angeheftete Beiträge", "column.public": "Föderierte Zeitleiste", + "column.status": "Toot", "column_back_button.label": "Zurück", "column_header.hide_settings": "Einstellungen verbergen", "column_header.moveLeft_settings": "Spalte nach links verschieben", diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index a46b9ee0ce..0d682c983e 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -1129,6 +1129,15 @@ ], "path": "app/javascript/mastodon/features/compose/components/upload_form.json" }, + { + "descriptors": [ + { + "defaultMessage": "Uploading...", + "id": "upload_progress.label" + } + ], + "path": "app/javascript/mastodon/features/compose/components/upload_progress.json" + }, { "descriptors": [ { @@ -1155,19 +1164,6 @@ ], "path": "app/javascript/mastodon/features/compose/containers/navigation_container.json" }, - { - "descriptors": [ - { - "defaultMessage": "Are you sure you want to log out?", - "id": "confirmations.logout.message" - }, - { - "defaultMessage": "Log out", - "id": "confirmations.logout.confirm" - } - ], - "path": "app/javascript/mastodon/features/compose/containers/navigation_container.json" - }, { "descriptors": [ { @@ -1584,10 +1580,6 @@ }, { "descriptors": [ - { - "defaultMessage": "Basic", - "id": "home.column_settings.basic" - }, { "defaultMessage": "Show boosts", "id": "home.column_settings.show_reblogs" @@ -1969,6 +1961,14 @@ "defaultMessage": "Push notifications", "id": "notifications.column_settings.push" }, + { + "defaultMessage": "Basic", + "id": "home.column_settings.basic" + }, + { + "defaultMessage": "Update in real-time", + "id": "home.column_settings.update_live" + }, { "defaultMessage": "Quick filter bar", "id": "notifications.column_settings.filter_bar.category" @@ -2027,6 +2027,10 @@ }, { "descriptors": [ + { + "defaultMessage": "and {count, plural, one {# other} other {# others}}", + "id": "notification.and_n_others" + }, { "defaultMessage": "{name} followed you", "id": "notification.follow" @@ -2283,6 +2287,10 @@ "defaultMessage": "Block & Report", "id": "confirmations.block.block_and_report" }, + { + "defaultMessage": "Toot", + "id": "column.status" + }, { "defaultMessage": "Are you sure you want to block {name}?", "id": "confirmations.block.message" diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json index 4c8a587781..bdd1da36cf 100644 --- a/app/javascript/mastodon/locales/el.json +++ b/app/javascript/mastodon/locales/el.json @@ -63,6 +63,7 @@ "column.notifications": "Ειδοποιήσεις", "column.pins": "Καρφιτσωμένα τουτ", "column.public": "Ομοσπονδιακή ροή", + "column.status": "Toot", "column_back_button.label": "Πίσω", "column_header.hide_settings": "Απόκρυψη ρυθμίσεων", "column_header.moveLeft_settings": "Μεταφορά κολώνας αριστερά", diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 260b43c53f..15c579d510 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -63,6 +63,7 @@ "column.notifications": "Notifications", "column.pins": "Pinned toots", "column.public": "Federated timeline", + "column.status": "Toot", "column_back_button.label": "Back", "column_header.hide_settings": "Hide settings", "column_header.moveLeft_settings": "Move column to the left", @@ -173,6 +174,7 @@ "home.column_settings.basic": "Basic", "home.column_settings.show_reblogs": "Show boosts", "home.column_settings.show_replies": "Show replies", + "home.column_settings.update_live": "Update in real-time", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -267,6 +269,7 @@ "navigation_bar.preferences": "Preferences", "navigation_bar.public_timeline": "Federated timeline", "navigation_bar.security": "Security", + "notification.and_n_others": "and {count, plural, one {# other} other {# others}}", "notification.favourite": "{name} favourited your status", "notification.follow": "{name} followed you", "notification.mention": "{name} mentioned you", diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json index a04a70cce2..31750050e0 100644 --- a/app/javascript/mastodon/locales/eo.json +++ b/app/javascript/mastodon/locales/eo.json @@ -63,6 +63,7 @@ "column.notifications": "Sciigoj", "column.pins": "Alpinglitaj mesaĝoj", "column.public": "Fratara tempolinio", + "column.status": "Toot", "column_back_button.label": "Reveni", "column_header.hide_settings": "Kaŝi agordojn", "column_header.moveLeft_settings": "Movi kolumnon maldekstren", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index 3b36571b1c..a033f6e1fc 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -4,7 +4,7 @@ "account.block": "Bloquear a @{name}", "account.block_domain": "Ocultar todo de {domain}", "account.blocked": "Bloqueado", - "account.cancel_follow_request": "Cancel follow request", + "account.cancel_follow_request": "Cancelar la solicitud de seguimiento", "account.direct": "Mensaje directo a @{name}", "account.domain_blocked": "Dominio oculto", "account.edit_profile": "Editar perfil", @@ -16,7 +16,7 @@ "account.follows.empty": "Este usuario todavía no sigue a nadie.", "account.follows_you": "Te sigue", "account.hide_reblogs": "Ocultar retoots de @{name}", - "account.last_status": "Last active", + "account.last_status": "Última actividad", "account.link_verified_on": "El proprietario de este link fue comprobado el {date}", "account.locked_info": "El estado de privacidad de esta cuenta està configurado como bloqueado. El proprietario debe revisar manualmente quien puede seguirle.", "account.media": "Multimedia", @@ -25,7 +25,7 @@ "account.mute": "Silenciar a @{name}", "account.mute_notifications": "Silenciar notificaciones de @{name}", "account.muted": "Silenciado", - "account.never_active": "Never", + "account.never_active": "Nunca", "account.posts": "Toots", "account.posts_with_replies": "Toots con respuestas", "account.report": "Reportar a @{name}", @@ -39,7 +39,7 @@ "account.unmute": "Dejar de silenciar a @{name}", "account.unmute_notifications": "Dejar de silenciar las notificaciones de @{name}", "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", - "alert.rate_limited.title": "Rate limited", + "alert.rate_limited.title": "Tarifa limitada", "alert.unexpected.message": "Hubo un error inesperado.", "alert.unexpected.title": "¡Ups!", "autosuggest_hashtag.per_week": "{count} per week", @@ -53,7 +53,7 @@ "column.blocks": "Usuarios bloqueados", "column.community": "Línea de tiempo local", "column.direct": "Mensajes directos", - "column.directory": "Browse profiles", + "column.directory": "Buscar perfiles", "column.domain_blocks": "Dominios ocultados", "column.favourites": "Favoritos", "column.follow_requests": "Solicitudes de seguimiento", @@ -63,6 +63,7 @@ "column.notifications": "Notificaciones", "column.pins": "Toots fijados", "column.public": "Línea de tiempo federada", + "column.status": "Toot", "column_back_button.label": "Atrás", "column_header.hide_settings": "Ocultar configuración", "column_header.moveLeft_settings": "Mover columna a la izquierda", @@ -100,8 +101,8 @@ "confirmations.delete_list.message": "¿Seguro que quieres borrar esta lista permanentemente?", "confirmations.domain_block.confirm": "Ocultar dominio entero", "confirmations.domain_block.message": "¿Seguro de que quieres bloquear al dominio {domain} entero? En general unos cuantos bloqueos y silenciados concretos es suficiente y preferible.", - "confirmations.logout.confirm": "Log out", - "confirmations.logout.message": "Are you sure you want to log out?", + "confirmations.logout.confirm": "Cerrar sesión", + "confirmations.logout.message": "¿Estás seguro de querer cerrar la sesión?", "confirmations.mute.confirm": "Silenciar", "confirmations.mute.message": "¿Estás seguro de que quieres silenciar a {name}?", "confirmations.redraft.confirm": "Borrar y volver a borrador", @@ -110,10 +111,10 @@ "confirmations.reply.message": "Responder sobrescribirá el mensaje que estás escribiendo. ¿Estás seguro de que deseas continuar?", "confirmations.unfollow.confirm": "Dejar de seguir", "confirmations.unfollow.message": "¿Estás seguro de que quieres dejar de seguir a {name}?", - "directory.federated": "From known fediverse", - "directory.local": "From {domain} only", - "directory.new_arrivals": "New arrivals", - "directory.recently_active": "Recently active", + "directory.federated": "Desde el fediverso conocido", + "directory.local": "Sólo de {domain}", + "directory.new_arrivals": "Recién llegados", + "directory.recently_active": "Recientemente activo", "embed.instructions": "Añade este toot a tu sitio web con el siguiente código.", "embed.preview": "Así es como se verá:", "emoji_button.activity": "Actividad", @@ -368,7 +369,7 @@ "status.show_more": "Mostrar más", "status.show_more_all": "Mostrar más para todo", "status.show_thread": "Ver hilo", - "status.uncached_media_warning": "Not available", + "status.uncached_media_warning": "No disponible", "status.unmute_conversation": "Dejar de silenciar conversación", "status.unpin": "Dejar de fijar", "suggestions.dismiss": "Descartar sugerencia", @@ -384,21 +385,21 @@ "time_remaining.moments": "Momentos restantes", "time_remaining.seconds": "{number, plural, one {# segundo restante} other {# segundos restantes}}", "trends.count_by_accounts": "{count} {rawCount, plural, one {persona} other {personas}} hablando", - "trends.trending_now": "Trending now", + "trends.trending_now": "Tendencia ahora", "ui.beforeunload": "Tu borrador se perderá si sales de Mastodon.", "upload_area.title": "Arrastra y suelta para subir", "upload_button.label": "Subir multimedia (JPEG, PNG, GIF, WebM, MP4, MOV)", "upload_error.limit": "Límite de subida de archivos excedido.", "upload_error.poll": "Subida de archivos no permitida con encuestas.", "upload_form.description": "Describir para los usuarios con dificultad visual", - "upload_form.edit": "Edit", + "upload_form.edit": "Editar", "upload_form.undo": "Borrar", - "upload_modal.analyzing_picture": "Analyzing picture…", - "upload_modal.apply": "Apply", - "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", - "upload_modal.detect_text": "Detect text from picture", - "upload_modal.edit_media": "Edit media", - "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.", + "upload_modal.analyzing_picture": "Analizando imagen…", + "upload_modal.apply": "Aplicar", + "upload_modal.description_placeholder": "Un rápido zorro marrón salta sobre el perro perezoso", + "upload_modal.detect_text": "Detectar texto de la imagen", + "upload_modal.edit_media": "Editar multimedia", + "upload_modal.hint": "Haga clic o arrastre el círculo en la vista previa para elegir el punto focal que siempre estará a la vista en todas las miniaturas.", "upload_modal.preview_label": "Preview ({ratio})", "upload_progress.label": "Subiendo…", "video.close": "Cerrar video", diff --git a/app/javascript/mastodon/locales/et.json b/app/javascript/mastodon/locales/et.json index 63253a1776..1d1dfd35ab 100644 --- a/app/javascript/mastodon/locales/et.json +++ b/app/javascript/mastodon/locales/et.json @@ -63,6 +63,7 @@ "column.notifications": "Teated", "column.pins": "Kinnitatud upitused", "column.public": "Föderatiivne ajajoon", + "column.status": "Toot", "column_back_button.label": "Tagasi", "column_header.hide_settings": "Peida sätted", "column_header.moveLeft_settings": "Liiguta tulp vasakule", diff --git a/app/javascript/mastodon/locales/eu.json b/app/javascript/mastodon/locales/eu.json index e88bcfff16..f1fc17fdd6 100644 --- a/app/javascript/mastodon/locales/eu.json +++ b/app/javascript/mastodon/locales/eu.json @@ -63,6 +63,7 @@ "column.notifications": "Jakinarazpenak", "column.pins": "Pinned toot", "column.public": "Federatutako denbora-lerroa", + "column.status": "Toot", "column_back_button.label": "Atzera", "column_header.hide_settings": "Ezkutatu ezarpenak", "column_header.moveLeft_settings": "Eraman zutabea ezkerrera", diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json index 632698c468..9382ec5ee0 100644 --- a/app/javascript/mastodon/locales/fa.json +++ b/app/javascript/mastodon/locales/fa.json @@ -63,6 +63,7 @@ "column.notifications": "اعلان‌ها", "column.pins": "نوشته‌های ثابت", "column.public": "نوشته‌های همه‌جا", + "column.status": "Toot", "column_back_button.label": "بازگشت", "column_header.hide_settings": "نهفتن تنظیمات", "column_header.moveLeft_settings": "انتقال ستون به راست", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index 8f8e9fc586..01b5edad1b 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -63,6 +63,7 @@ "column.notifications": "Ilmoitukset", "column.pins": "Kiinnitetty tuuttaus", "column.public": "Yleinen aikajana", + "column.status": "Toot", "column_back_button.label": "Takaisin", "column_header.hide_settings": "Piilota asetukset", "column_header.moveLeft_settings": "Siirrä saraketta vasemmalle", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index 72158c413e..7cfe9829ad 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -63,6 +63,7 @@ "column.notifications": "Notifications", "column.pins": "Pouets épinglés", "column.public": "Fil public global", + "column.status": "Toot", "column_back_button.label": "Retour", "column_header.hide_settings": "Masquer les paramètres", "column_header.moveLeft_settings": "Déplacer la colonne vers la gauche", diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json index 1bf37c8985..3cc44f43ee 100644 --- a/app/javascript/mastodon/locales/gl.json +++ b/app/javascript/mastodon/locales/gl.json @@ -63,6 +63,7 @@ "column.notifications": "Notificacións", "column.pins": "Mensaxes fixadas", "column.public": "Liña temporal federada", + "column.status": "Toot", "column_back_button.label": "Atrás", "column_header.hide_settings": "Agochar axustes", "column_header.moveLeft_settings": "Mover a columna hacia a esquerda", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index fd7e40c532..b6cc3e6ce1 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -63,6 +63,7 @@ "column.notifications": "התראות", "column.pins": "Pinned toot", "column.public": "בפרהסיה", + "column.status": "Toot", "column_back_button.label": "חזרה", "column_header.hide_settings": "הסתרת העדפות", "column_header.moveLeft_settings": "הזחת טור לשמאל", diff --git a/app/javascript/mastodon/locales/hi.json b/app/javascript/mastodon/locales/hi.json index 55b383d599..e2d1eb49d1 100644 --- a/app/javascript/mastodon/locales/hi.json +++ b/app/javascript/mastodon/locales/hi.json @@ -63,6 +63,7 @@ "column.notifications": "Notifications", "column.pins": "Pinned toot", "column.public": "Federated timeline", + "column.status": "Toot", "column_back_button.label": "Back", "column_header.hide_settings": "Hide settings", "column_header.moveLeft_settings": "Move column to the left", diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json index 8d7cb436ce..6daabc6940 100644 --- a/app/javascript/mastodon/locales/hr.json +++ b/app/javascript/mastodon/locales/hr.json @@ -63,6 +63,7 @@ "column.notifications": "Notifikacije", "column.pins": "Pinned toot", "column.public": "Federalni timeline", + "column.status": "Toot", "column_back_button.label": "Natrag", "column_header.hide_settings": "Hide settings", "column_header.moveLeft_settings": "Move column to the left", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index 513f2a22a7..f5a02065bf 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -63,6 +63,7 @@ "column.notifications": "Értesítések", "column.pins": "Kitűzött tülkök", "column.public": "Nyilvános idővonal", + "column.status": "Toot", "column_back_button.label": "Vissza", "column_header.hide_settings": "Beállítások elrejtése", "column_header.moveLeft_settings": "Oszlop elmozdítása balra", diff --git a/app/javascript/mastodon/locales/hy.json b/app/javascript/mastodon/locales/hy.json index 1c3f1eec0a..1484c76df2 100644 --- a/app/javascript/mastodon/locales/hy.json +++ b/app/javascript/mastodon/locales/hy.json @@ -63,6 +63,7 @@ "column.notifications": "Ծանուցումներ", "column.pins": "Ամրացված թթեր", "column.public": "Դաշնային հոսք", + "column.status": "Toot", "column_back_button.label": "Ետ", "column_header.hide_settings": "Թաքցնել կարգավորումները", "column_header.moveLeft_settings": "Տեղաշարժել սյունը ձախ", diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json index 5e1f318be6..c9e48a1a6f 100644 --- a/app/javascript/mastodon/locales/id.json +++ b/app/javascript/mastodon/locales/id.json @@ -63,6 +63,7 @@ "column.notifications": "Notifikasi", "column.pins": "Pinned toot", "column.public": "Linimasa gabungan", + "column.status": "Toot", "column_back_button.label": "Kembali", "column_header.hide_settings": "Sembunyikan pengaturan", "column_header.moveLeft_settings": "Pindahkan kolom ke kiri", diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json index afbd970ecb..6c1b7fa8ba 100644 --- a/app/javascript/mastodon/locales/io.json +++ b/app/javascript/mastodon/locales/io.json @@ -63,6 +63,7 @@ "column.notifications": "Savigi", "column.pins": "Pinned toot", "column.public": "Federata tempolineo", + "column.status": "Toot", "column_back_button.label": "Retro", "column_header.hide_settings": "Hide settings", "column_header.moveLeft_settings": "Move column to the left", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index caabf6ef3e..dc43bcb5cc 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -63,6 +63,7 @@ "column.notifications": "Notifiche", "column.pins": "Toot fissati in cima", "column.public": "Timeline federata", + "column.status": "Toot", "column_back_button.label": "Indietro", "column_header.hide_settings": "Nascondi impostazioni", "column_header.moveLeft_settings": "Sposta colonna a sinistra", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index 86c2e90ad5..64baed71d0 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -63,6 +63,7 @@ "column.notifications": "通知", "column.pins": "固定されたトゥート", "column.public": "連合タイムライン", + "column.status": "Toot", "column_back_button.label": "戻る", "column_header.hide_settings": "設定を隠す", "column_header.moveLeft_settings": "カラムを左に移動する", diff --git a/app/javascript/mastodon/locales/ka.json b/app/javascript/mastodon/locales/ka.json index d3018c0bf2..e2a6ee6c63 100644 --- a/app/javascript/mastodon/locales/ka.json +++ b/app/javascript/mastodon/locales/ka.json @@ -63,6 +63,7 @@ "column.notifications": "შეტყობინებები", "column.pins": "აპინული ტუტები", "column.public": "ფედერალური თაიმლაინი", + "column.status": "Toot", "column_back_button.label": "უკან", "column_header.hide_settings": "პარამეტრების დამალვა", "column_header.moveLeft_settings": "სვეტის მარცხნივ გადატანა", diff --git a/app/javascript/mastodon/locales/kk.json b/app/javascript/mastodon/locales/kk.json index 5d671d9074..a07302f0a4 100644 --- a/app/javascript/mastodon/locales/kk.json +++ b/app/javascript/mastodon/locales/kk.json @@ -63,6 +63,7 @@ "column.notifications": "Ескертпелер", "column.pins": "Жабыстырылған жазбалар", "column.public": "Жаһандық желі", + "column.status": "Toot", "column_back_button.label": "Артқа", "column_header.hide_settings": "Баптауларды жасыр", "column_header.moveLeft_settings": "Бағананы солға жылжыту", diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json index 50f7ca543e..3ec9a8a164 100644 --- a/app/javascript/mastodon/locales/ko.json +++ b/app/javascript/mastodon/locales/ko.json @@ -63,6 +63,7 @@ "column.notifications": "알림", "column.pins": "고정된 툿", "column.public": "연합 타임라인", + "column.status": "Toot", "column_back_button.label": "돌아가기", "column_header.hide_settings": "설정 숨기기", "column_header.moveLeft_settings": "왼쪽으로 이동", @@ -292,7 +293,7 @@ "notifications.group": "{count} 개의 알림", "poll.closed": "마감됨", "poll.refresh": "새로고침", - "poll.total_votes": "{count} 명 참여", + "poll.total_votes": "{count} 표", "poll.vote": "투표", "poll_button.add_poll": "투표 추가", "poll_button.remove_poll": "투표 삭제", diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json index 7d0776dff4..2de037f16c 100644 --- a/app/javascript/mastodon/locales/lt.json +++ b/app/javascript/mastodon/locales/lt.json @@ -63,6 +63,7 @@ "column.notifications": "Notifications", "column.pins": "Pinned toot", "column.public": "Federated timeline", + "column.status": "Toot", "column_back_button.label": "Back", "column_header.hide_settings": "Hide settings", "column_header.moveLeft_settings": "Move column to the left", diff --git a/app/javascript/mastodon/locales/lv.json b/app/javascript/mastodon/locales/lv.json index d9b1256951..8d281c9d5b 100644 --- a/app/javascript/mastodon/locales/lv.json +++ b/app/javascript/mastodon/locales/lv.json @@ -63,6 +63,7 @@ "column.notifications": "Paziņojumi", "column.pins": "Piespraustie ziņojumi", "column.public": "Federatīvā laika līnija", + "column.status": "Toot", "column_back_button.label": "Atpakaļ", "column_header.hide_settings": "Paslēpt iestatījumus", "column_header.moveLeft_settings": "Pārvietot kolonu pa kreisi", diff --git a/app/javascript/mastodon/locales/ms.json b/app/javascript/mastodon/locales/ms.json index b83d26a0a5..9bd5eef721 100644 --- a/app/javascript/mastodon/locales/ms.json +++ b/app/javascript/mastodon/locales/ms.json @@ -63,6 +63,7 @@ "column.notifications": "Notifications", "column.pins": "Pinned toot", "column.public": "Federated timeline", + "column.status": "Toot", "column_back_button.label": "Back", "column_header.hide_settings": "Hide settings", "column_header.moveLeft_settings": "Move column to the left", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index 439dccbb3e..73e7b39050 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -63,6 +63,7 @@ "column.notifications": "Meldingen", "column.pins": "Vastgezette toots", "column.public": "Globale tijdlijn", + "column.status": "Toot", "column_back_button.label": "Terug", "column_header.hide_settings": "Instellingen verbergen", "column_header.moveLeft_settings": "Kolom naar links verplaatsen", diff --git a/app/javascript/mastodon/locales/nn.json b/app/javascript/mastodon/locales/nn.json new file mode 100644 index 0000000000..dda4024940 --- /dev/null +++ b/app/javascript/mastodon/locales/nn.json @@ -0,0 +1,414 @@ +{ + "account.add_or_remove_from_list": "Legg til eller ta vekk fra liste", + "account.badges.bot": "Robot", + "account.block": "Blokkér @{name}", + "account.block_domain": "Gøyme alt innhald for domenet {domain}", + "account.blocked": "Blokkert", + "account.cancel_follow_request": "Avslutt føljar-førespurnad", + "account.direct": "Direkte meld @{name}", + "account.domain_blocked": "Domenet er gøymt", + "account.edit_profile": "Rediger profil", + "account.endorse": "Framhev på profilen din", + "account.follow": "Følj", + "account.followers": "Føljare", + "account.followers.empty": "Er ikkje nokon som føljar denne brukaren ennå.", + "account.follows": "Føljingar", + "account.follows.empty": "Denne brukaren foljer ikkje nokon ennå.", + "account.follows_you": "Føljar deg", + "account.hide_reblogs": "Gøym robotar for @{name}", + "account.last_status": "Sist aktiv", + "account.link_verified_on": "Eigerskap for denne linken er sist sjekket den {date}", + "account.locked_info": "Brukarens privat-status er satt til lukka. Eigaren må manuelt døme kvem som kan følje honom.", + "account.media": "Media", + "account.mention": "Nemne @{name}", + "account.moved_to": "{name} har flytta til:", + "account.mute": "Målbind @{name}", + "account.mute_notifications": "Målbind notifikasjoner ifrå @{name}", + "account.muted": "Målbindt", + "account.never_active": "Aldri", + "account.posts": "Tutar", + "account.posts_with_replies": "Tutar og svar", + "account.report": "Rapporter @{name}", + "account.requested": "Venter på samtykke. Klikk for å avbryte føljar-førespurnad", + "account.share": "Del @{name} sin profil", + "account.show_reblogs": "Sjå framhevingar ifrå @{name}", + "account.unblock": "Avblokker @{name}", + "account.unblock_domain": "Vis {domain}", + "account.unendorse": "Ikkje framhev på profil", + "account.unfollow": "Avfølja", + "account.unmute": "Av-demp @{name}", + "account.unmute_notifications": "Av-demp notifikasjoner ifrå @{name}", + "alert.rate_limited.message": "Ver vennlig og prøv igjen {retry_time, time, medium}.", + "alert.rate_limited.title": "Bregrensa rate", + "alert.unexpected.message": "Eit uforventa problem har hendt.", + "alert.unexpected.title": "Oops!", + "autosuggest_hashtag.per_week": "{count} per veke", + "boost_modal.combo": "Du kan trykke {combo} for å hoppe over dette neste gong", + "bundle_column_error.body": "Noko gikk gale mens komponent ble nedlasta.", + "bundle_column_error.retry": "Prøv igjen", + "bundle_column_error.title": "Tenarmaskin feil", + "bundle_modal_error.close": "Lukk", + "bundle_modal_error.message": "Noko gikk gale mens komponent var i ferd med å bli nedlasta.", + "bundle_modal_error.retry": "Prøv igjen", + "column.blocks": "Blokka brukare", + "column.community": "Lokal samtid", + "column.direct": "Direkte meldingar", + "column.directory": "Sjå gjennom profiler", + "column.domain_blocks": "Gøymte domener", + "column.favourites": "Favorittar", + "column.follow_requests": "Føljarførespurnad", + "column.home": "Heim", + "column.lists": "Lister", + "column.mutes": "Målbindte brukare", + "column.notifications": "Varslinger", + "column.pins": "Festa tuter", + "column.public": "Federert samtid", + "column.status": "Toot", + "column_back_button.label": "Tilbake", + "column_header.hide_settings": "Skjul innstillingar", + "column_header.moveLeft_settings": "Flytt feltet til venstre", + "column_header.moveRight_settings": "Flytt feltet til høgre", + "column_header.pin": "Fest", + "column_header.show_settings": "Vis innstillingar", + "column_header.unpin": "Løys", + "column_subheading.settings": "Innstillingar", + "community.column_settings.media_only": "Kun medie", + "compose_form.direct_message_warning": "Denne tuten vil kun verte synleg for nemnde brukarar.", + "compose_form.direct_message_warning_learn_more": "Lær meir", + "compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.lock_disclaimer.lock": "locked", + "compose_form.placeholder": "What is on your mind?", + "compose_form.poll.add_option": "Add a choice", + "compose_form.poll.duration": "Poll duration", + "compose_form.poll.option_placeholder": "Choice {number}", + "compose_form.poll.remove_option": "Remove this choice", + "compose_form.publish": "Toot", + "compose_form.publish_loud": "{publish}!", + "compose_form.sensitive.hide": "Mark media as sensitive", + "compose_form.sensitive.marked": "Media is marked as sensitive", + "compose_form.sensitive.unmarked": "Media is not marked as sensitive", + "compose_form.spoiler.marked": "Text is hidden behind warning", + "compose_form.spoiler.unmarked": "Text is not hidden", + "compose_form.spoiler_placeholder": "Write your warning here", + "confirmation_modal.cancel": "Cancel", + "confirmations.block.block_and_report": "Block & Report", + "confirmations.block.confirm": "Block", + "confirmations.block.message": "Are you sure you want to block {name}?", + "confirmations.delete.confirm": "Delete", + "confirmations.delete.message": "Are you sure you want to delete this status?", + "confirmations.delete_list.confirm": "Delete", + "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", + "confirmations.domain_block.confirm": "Hide entire domain", + "confirmations.domain_block.message": "Er du ordentleg, ordentleg sikker på at du vill blokkere heile {domain}? I dei tilfeller er det bedre med ein målretta blokkering eller demping av individuelle brukare.", + "confirmations.logout.confirm": "Logg ut", + "confirmations.logout.message": "Er du sikker på at du vill logge ut?", + "confirmations.mute.confirm": "Målbind", + "confirmations.mute.message": "Er du sikker på at d vill målbinde {name}?", + "confirmations.redraft.confirm": "Slett & gjennopprett", + "confirmations.redraft.message": "Er du sikker på at du vill slette statusen og gjennoprette den? Favoritter og framhevinger vill bli borte, og svar til den originale posten vill bli einstøing.", + "confirmations.reply.confirm": "Svar", + "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", + "confirmations.unfollow.confirm": "Unfollow", + "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", + "directory.federated": "From known fediverse", + "directory.local": "From {domain} only", + "directory.new_arrivals": "New arrivals", + "directory.recently_active": "Recently active", + "embed.instructions": "Embed this status on your website by copying the code below.", + "embed.preview": "Here is what it will look like:", + "emoji_button.activity": "Activity", + "emoji_button.custom": "Custom", + "emoji_button.flags": "Flags", + "emoji_button.food": "Food & Drink", + "emoji_button.label": "Insert emoji", + "emoji_button.nature": "Nature", + "emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻", + "emoji_button.objects": "Objects", + "emoji_button.people": "People", + "emoji_button.recent": "Frequently used", + "emoji_button.search": "Search...", + "emoji_button.search_results": "Search results", + "emoji_button.symbols": "Symbols", + "emoji_button.travel": "Travel & Places", + "empty_column.account_timeline": "No toots here!", + "empty_column.account_unavailable": "Profile unavailable", + "empty_column.blocks": "You haven't blocked any users yet.", + "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", + "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.", + "empty_column.domain_blocks": "There are no hidden domains yet.", + "empty_column.favourited_statuses": "You don't have any favourite toots yet. When you favourite one, it will show up here.", + "empty_column.favourites": "No one has favourited this toot yet. When someone does, they will show up here.", + "empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.", + "empty_column.hashtag": "There is nothing in this hashtag yet.", + "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.", + "empty_column.home.public_timeline": "the public timeline", + "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.", + "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.", + "empty_column.mutes": "You haven't muted any users yet.", + "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.", + "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other servers to fill it up", + "follow_request.authorize": "Authorize", + "follow_request.reject": "Reject", + "getting_started.developers": "Developers", + "getting_started.directory": "Profile directory", + "getting_started.documentation": "Documentation", + "getting_started.heading": "Getting started", + "getting_started.invite": "Invite people", + "getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}.", + "getting_started.security": "Security", + "getting_started.terms": "Terms of service", + "hashtag.column_header.tag_mode.all": "and {additional}", + "hashtag.column_header.tag_mode.any": "or {additional}", + "hashtag.column_header.tag_mode.none": "without {additional}", + "hashtag.column_settings.select.no_options_message": "No suggestions found", + "hashtag.column_settings.select.placeholder": "Enter hashtags…", + "hashtag.column_settings.tag_mode.all": "All of these", + "hashtag.column_settings.tag_mode.any": "Any of these", + "hashtag.column_settings.tag_mode.none": "None of these", + "hashtag.column_settings.tag_toggle": "Include additional tags in this column", + "home.column_settings.basic": "Basic", + "home.column_settings.show_reblogs": "Show boosts", + "home.column_settings.show_replies": "Show replies", + "home.column_settings.update_live": "Update in real-time", + "intervals.full.days": "{number, plural, one {# day} other {# days}}", + "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", + "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", + "introduction.federation.action": "Next", + "introduction.federation.federated.headline": "Federated", + "introduction.federation.federated.text": "Public posts from other servers of the fediverse will appear in the federated timeline.", + "introduction.federation.home.headline": "Home", + "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", + "introduction.federation.local.headline": "Local", + "introduction.federation.local.text": "Public posts from people on the same server as you will appear in the local timeline.", + "introduction.interactions.action": "Finish toot-orial!", + "introduction.interactions.favourite.headline": "Favourite", + "introduction.interactions.favourite.text": "You can save a toot for later, and let the author know that you liked it, by favouriting it.", + "introduction.interactions.reblog.headline": "Boost", + "introduction.interactions.reblog.text": "You can share other people's toots with your followers by boosting them.", + "introduction.interactions.reply.headline": "Reply", + "introduction.interactions.reply.text": "You can reply to other people's and your own toots, which will chain them together in a conversation.", + "introduction.welcome.action": "Let's go!", + "introduction.welcome.headline": "First steps", + "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.", + "keyboard_shortcuts.back": "to navigate back", + "keyboard_shortcuts.blocked": "to open blocked users list", + "keyboard_shortcuts.boost": "to boost", + "keyboard_shortcuts.column": "to focus a status in one of the columns", + "keyboard_shortcuts.compose": "to focus the compose textarea", + "keyboard_shortcuts.description": "Description", + "keyboard_shortcuts.direct": "to open direct messages column", + "keyboard_shortcuts.down": "to move down in the list", + "keyboard_shortcuts.enter": "to open status", + "keyboard_shortcuts.favourite": "to favourite", + "keyboard_shortcuts.favourites": "to open favourites list", + "keyboard_shortcuts.federated": "to open federated timeline", + "keyboard_shortcuts.heading": "Keyboard Shortcuts", + "keyboard_shortcuts.home": "to open home timeline", + "keyboard_shortcuts.hotkey": "Hotkey", + "keyboard_shortcuts.legend": "to display this legend", + "keyboard_shortcuts.local": "to open local timeline", + "keyboard_shortcuts.mention": "to mention author", + "keyboard_shortcuts.muted": "to open muted users list", + "keyboard_shortcuts.my_profile": "to open your profile", + "keyboard_shortcuts.notifications": "to open notifications column", + "keyboard_shortcuts.pinned": "to open pinned toots list", + "keyboard_shortcuts.profile": "to open author's profile", + "keyboard_shortcuts.reply": "to reply", + "keyboard_shortcuts.requests": "to open follow requests list", + "keyboard_shortcuts.search": "to focus search", + "keyboard_shortcuts.start": "to open \"get started\" column", + "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", + "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", + "keyboard_shortcuts.toot": "to start a brand new toot", + "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", + "keyboard_shortcuts.up": "to move up in the list", + "lightbox.close": "Close", + "lightbox.next": "Next", + "lightbox.previous": "Previous", + "lightbox.view_context": "View context", + "lists.account.add": "Add to list", + "lists.account.remove": "Remove from list", + "lists.delete": "Delete list", + "lists.edit": "Edit list", + "lists.edit.submit": "Change title", + "lists.new.create": "Add list", + "lists.new.title_placeholder": "New list title", + "lists.search": "Search among people you follow", + "lists.subheading": "Your lists", + "load_pending": "{count, plural, one {# new item} other {# new items}}", + "loading_indicator.label": "Loading...", + "media_gallery.toggle_visible": "Toggle visibility", + "missing_indicator.label": "Not found", + "missing_indicator.sublabel": "This resource could not be found", + "mute_modal.hide_notifications": "Hide notifications from this user?", + "navigation_bar.apps": "Mobile apps", + "navigation_bar.blocks": "Blocked users", + "navigation_bar.community_timeline": "Local timeline", + "navigation_bar.compose": "Compose new toot", + "navigation_bar.direct": "Direct messages", + "navigation_bar.discover": "Discover", + "navigation_bar.domain_blocks": "Hidden domains", + "navigation_bar.edit_profile": "Edit profile", + "navigation_bar.favourites": "Favourites", + "navigation_bar.filters": "Muted words", + "navigation_bar.follow_requests": "Follow requests", + "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.info": "About this server", + "navigation_bar.keyboard_shortcuts": "Hotkeys", + "navigation_bar.lists": "Lists", + "navigation_bar.logout": "Logout", + "navigation_bar.mutes": "Muted users", + "navigation_bar.personal": "Personal", + "navigation_bar.pins": "Pinned toots", + "navigation_bar.preferences": "Preferences", + "navigation_bar.public_timeline": "Federated timeline", + "navigation_bar.security": "Security", + "notification.and_n_others": "and {count, plural, one {# other} other {# others}}", + "notification.favourite": "{name} favourited your status", + "notification.follow": "{name} followed you", + "notification.mention": "{name} mentioned you", + "notification.poll": "A poll you have voted in has ended", + "notification.reblog": "{name} boosted your status", + "notifications.clear": "Clear notifications", + "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", + "notifications.column_settings.alert": "Desktop notifications", + "notifications.column_settings.favourite": "Favourites:", + "notifications.column_settings.filter_bar.advanced": "Display all categories", + "notifications.column_settings.filter_bar.category": "Quick filter bar", + "notifications.column_settings.filter_bar.show": "Show", + "notifications.column_settings.follow": "New followers:", + "notifications.column_settings.mention": "Mentions:", + "notifications.column_settings.poll": "Poll results:", + "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.reblog": "Boosts:", + "notifications.column_settings.show": "Show in column", + "notifications.column_settings.sound": "Play sound", + "notifications.filter.all": "All", + "notifications.filter.boosts": "Boosts", + "notifications.filter.favourites": "Favourites", + "notifications.filter.follows": "Follows", + "notifications.filter.mentions": "Mentions", + "notifications.filter.polls": "Poll results", + "notifications.group": "{count} notifications", + "poll.closed": "Closed", + "poll.refresh": "Refresh", + "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", + "poll.vote": "Vote", + "poll_button.add_poll": "Add a poll", + "poll_button.remove_poll": "Remove poll", + "privacy.change": "Adjust status privacy", + "privacy.direct.long": "Post to mentioned users only", + "privacy.direct.short": "Direct", + "privacy.private.long": "Post to followers only", + "privacy.private.short": "Followers-only", + "privacy.public.long": "Post to public timelines", + "privacy.public.short": "Public", + "privacy.unlisted.long": "Do not show in public timelines", + "privacy.unlisted.short": "Unlisted", + "regeneration_indicator.label": "Loading…", + "regeneration_indicator.sublabel": "Your home feed is being prepared!", + "relative_time.days": "{number}d", + "relative_time.hours": "{number}h", + "relative_time.just_now": "now", + "relative_time.minutes": "{number}m", + "relative_time.seconds": "{number}s", + "reply_indicator.cancel": "Cancel", + "report.forward": "Forward to {target}", + "report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:", + "report.placeholder": "Additional comments", + "report.submit": "Submit", + "report.target": "Report {target}", + "search.placeholder": "Search", + "search_popout.search_format": "Advanced search format", + "search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", + "search_popout.tips.hashtag": "hashtag", + "search_popout.tips.status": "status", + "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", + "search_popout.tips.user": "user", + "search_results.accounts": "People", + "search_results.hashtags": "Hashtags", + "search_results.statuses": "Toots", + "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", + "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "status.admin_account": "Open moderation interface for @{name}", + "status.admin_status": "Open this status in the moderation interface", + "status.block": "Block @{name}", + "status.cancel_reblog_private": "Unboost", + "status.cannot_reblog": "This post cannot be boosted", + "status.copy": "Copy link to status", + "status.delete": "Delete", + "status.detailed_status": "Detailed conversation view", + "status.direct": "Direct message @{name}", + "status.embed": "Embed", + "status.favourite": "Favourite", + "status.filtered": "Filtered", + "status.load_more": "Load more", + "status.media_hidden": "Media hidden", + "status.mention": "Mention @{name}", + "status.more": "More", + "status.mute": "Mute @{name}", + "status.mute_conversation": "Mute conversation", + "status.open": "Expand this status", + "status.pin": "Pin on profile", + "status.pinned": "Pinned toot", + "status.read_more": "Read more", + "status.reblog": "Boost", + "status.reblog_private": "Boost to original audience", + "status.reblogged_by": "{name} boosted", + "status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.", + "status.redraft": "Delete & re-draft", + "status.reply": "Reply", + "status.replyAll": "Reply to thread", + "status.report": "Report @{name}", + "status.sensitive_warning": "Sensitive content", + "status.share": "Share", + "status.show_less": "Show less", + "status.show_less_all": "Show less for all", + "status.show_more": "Show more", + "status.show_more_all": "Show more for all", + "status.show_thread": "Show thread", + "status.uncached_media_warning": "Not available", + "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", + "suggestions.dismiss": "Dismiss suggestion", + "suggestions.header": "You might be interested in…", + "tabs_bar.federated_timeline": "Federated", + "tabs_bar.home": "Home", + "tabs_bar.local_timeline": "Local", + "tabs_bar.notifications": "Notifications", + "tabs_bar.search": "Search", + "time_remaining.days": "{number, plural, one {# day} other {# days}} left", + "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", + "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", + "time_remaining.moments": "Moments remaining", + "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", + "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", + "trends.trending_now": "Trending now", + "ui.beforeunload": "Your draft will be lost if you leave Mastodon.", + "upload_area.title": "Drag & drop to upload", + "upload_button.label": "Add media ({formats})", + "upload_error.limit": "File upload limit exceeded.", + "upload_error.poll": "File upload not allowed with polls.", + "upload_form.description": "Describe for the visually impaired", + "upload_form.edit": "Edit", + "upload_form.undo": "Delete", + "upload_modal.analyzing_picture": "Analyzing picture…", + "upload_modal.apply": "Apply", + "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog", + "upload_modal.detect_text": "Detect text from picture", + "upload_modal.edit_media": "Edit media", + "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.", + "upload_modal.preview_label": "Preview ({ratio})", + "upload_progress.label": "Uploading...", + "video.close": "Close video", + "video.exit_fullscreen": "Exit full screen", + "video.expand": "Expand video", + "video.fullscreen": "Full screen", + "video.hide": "Hide video", + "video.mute": "Mute sound", + "video.pause": "Pause", + "video.play": "Play", + "video.unmute": "Unmute sound" +} diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json index 77ddad7e01..8fc722037c 100644 --- a/app/javascript/mastodon/locales/no.json +++ b/app/javascript/mastodon/locales/no.json @@ -63,6 +63,7 @@ "column.notifications": "Varsler", "column.pins": "Pinned toot", "column.public": "Felles tidslinje", + "column.status": "Toot", "column_back_button.label": "Tilbake", "column_header.hide_settings": "Gjem innstillinger", "column_header.moveLeft_settings": "Flytt feltet til venstre", diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json index 10501796df..d5abe89fb9 100644 --- a/app/javascript/mastodon/locales/oc.json +++ b/app/javascript/mastodon/locales/oc.json @@ -63,6 +63,7 @@ "column.notifications": "Notificacions", "column.pins": "Tuts penjats", "column.public": "Flux public global", + "column.status": "Toot", "column_back_button.label": "Tornar", "column_header.hide_settings": "Amagar los paramètres", "column_header.moveLeft_settings": "Desplaçar la colomna a man drecha", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index 10d6d6453b..395f3927a5 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -63,6 +63,7 @@ "column.notifications": "Powiadomienia", "column.pins": "Przypięte wpisy", "column.public": "Globalna oś czasu", + "column.status": "Toot", "column_back_button.label": "Wróć", "column_header.hide_settings": "Ukryj ustawienia", "column_header.moveLeft_settings": "Przesuń kolumnę w lewo", diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index e11141f6c6..debf9e6f6c 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -63,6 +63,7 @@ "column.notifications": "Notificações", "column.pins": "Postagens fixadas", "column.public": "Global", + "column.status": "Toot", "column_back_button.label": "Voltar", "column_header.hide_settings": "Esconder configurações", "column_header.moveLeft_settings": "Mover coluna para a esquerda", diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt-PT.json similarity index 99% rename from app/javascript/mastodon/locales/pt.json rename to app/javascript/mastodon/locales/pt-PT.json index 63a078c4e9..feba8fd9a6 100644 --- a/app/javascript/mastodon/locales/pt.json +++ b/app/javascript/mastodon/locales/pt-PT.json @@ -63,6 +63,7 @@ "column.notifications": "Notificações", "column.pins": "Publicações fixas", "column.public": "Cronologia federada", + "column.status": "Toot", "column_back_button.label": "Voltar", "column_header.hide_settings": "Esconder configurações", "column_header.moveLeft_settings": "Mover coluna para a esquerda", diff --git a/app/javascript/mastodon/locales/ro.json b/app/javascript/mastodon/locales/ro.json index 27e4addda7..038b8ddd45 100644 --- a/app/javascript/mastodon/locales/ro.json +++ b/app/javascript/mastodon/locales/ro.json @@ -63,6 +63,7 @@ "column.notifications": "Notificări", "column.pins": "Postări fixate", "column.public": "Flux global", + "column.status": "Toot", "column_back_button.label": "Înapoi", "column_header.hide_settings": "Ascunde setările", "column_header.moveLeft_settings": "Mută coloana la stânga", diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json index efbaa25a08..69bd5a422c 100644 --- a/app/javascript/mastodon/locales/ru.json +++ b/app/javascript/mastodon/locales/ru.json @@ -63,6 +63,7 @@ "column.notifications": "Уведомления", "column.pins": "Закреплённый пост", "column.public": "Глобальная лента", + "column.status": "Toot", "column_back_button.label": "Назад", "column_header.hide_settings": "Скрыть настройки", "column_header.moveLeft_settings": "Передвинуть колонку влево", @@ -260,7 +261,7 @@ "navigation_bar.mutes": "Список скрытых пользователей", "navigation_bar.personal": "Личное", "navigation_bar.pins": "Закреплённые посты", - "navigation_bar.preferences": "Опции", + "navigation_bar.preferences": "Настройки", "navigation_bar.public_timeline": "Глобальная лента", "navigation_bar.security": "Безопасность", "notification.and_n_others": "and {count, plural, one {# other} other {# others}}", @@ -383,8 +384,8 @@ "time_remaining.minutes": "{number, plural, one {осталась # минута} few {осталось # минуты} many {осталось # минут} other {осталось # минут}}", "time_remaining.moments": "остались считанные мгновения", "time_remaining.seconds": "{number, plural, one {осталась # секунду} few {осталось # секунды} many {осталось # секунд} other {осталось # секунд}}", - "trends.count_by_accounts": "Популярно у {count} {rawCount, plural, one {человека} few {человек} many {человек} other {человек}}", - "trends.trending_now": "Trending now", + "trends.count_by_accounts": "{count} {rawCount, plural, one {человек говорит} few {человека говорят} other {человек говорят}} про это", + "trends.trending_now": "Самое актуальное", "ui.beforeunload": "Ваш черновик будет утерян, если вы покинете Mastodon.", "upload_area.title": "Перетащите сюда, чтобы загрузить", "upload_button.label": "Добавить медиаконтент", diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json index 312f633015..89a472d891 100644 --- a/app/javascript/mastodon/locales/sk.json +++ b/app/javascript/mastodon/locales/sk.json @@ -63,6 +63,7 @@ "column.notifications": "Oboznámenia", "column.pins": "Pripnuté príspevky", "column.public": "Federovaná časová os", + "column.status": "Toot", "column_back_button.label": "Späť", "column_header.hide_settings": "Skryť nastavenia", "column_header.moveLeft_settings": "Presuň stĺpec doľava", diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json index fa5d22fd14..d7d78c41cf 100644 --- a/app/javascript/mastodon/locales/sl.json +++ b/app/javascript/mastodon/locales/sl.json @@ -63,6 +63,7 @@ "column.notifications": "Obvestila", "column.pins": "Pripeti tuti", "column.public": "Združena časovnica", + "column.status": "Toot", "column_back_button.label": "Nazaj", "column_header.hide_settings": "Skrij nastavitve", "column_header.moveLeft_settings": "Premakni stolpec na levo", diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json index 12f66cafd3..0f851051c2 100644 --- a/app/javascript/mastodon/locales/sq.json +++ b/app/javascript/mastodon/locales/sq.json @@ -63,6 +63,7 @@ "column.notifications": "Njoftime", "column.pins": "Mesazhe të fiksuar", "column.public": "Rrjedhë kohore e federuar", + "column.status": "Toot", "column_back_button.label": "Mbrapsht", "column_header.hide_settings": "Fshihi rregullimet", "column_header.moveLeft_settings": "Shpjere shtyllën majtas", diff --git a/app/javascript/mastodon/locales/sr-Latn.json b/app/javascript/mastodon/locales/sr-Latn.json index 72ea3490ff..fb6a365cea 100644 --- a/app/javascript/mastodon/locales/sr-Latn.json +++ b/app/javascript/mastodon/locales/sr-Latn.json @@ -63,6 +63,7 @@ "column.notifications": "Obaveštenja", "column.pins": "Prikačeni tutovi", "column.public": "Federisana lajna", + "column.status": "Toot", "column_back_button.label": "Nazad", "column_header.hide_settings": "Sakrij postavke", "column_header.moveLeft_settings": "Pomeri kolonu ulevo", diff --git a/app/javascript/mastodon/locales/sr.json b/app/javascript/mastodon/locales/sr.json index c77927ec1a..064934f545 100644 --- a/app/javascript/mastodon/locales/sr.json +++ b/app/javascript/mastodon/locales/sr.json @@ -63,6 +63,7 @@ "column.notifications": "Обавештења", "column.pins": "Прикачене трубе", "column.public": "Здружена временска линија", + "column.status": "Toot", "column_back_button.label": "Назад", "column_header.hide_settings": "Сакриј поставке", "column_header.moveLeft_settings": "Помери колону улево", diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json index 6783da15d6..f666a4b6ee 100644 --- a/app/javascript/mastodon/locales/sv.json +++ b/app/javascript/mastodon/locales/sv.json @@ -63,6 +63,7 @@ "column.notifications": "Meddelanden", "column.pins": "Nålade toots", "column.public": "Förenad tidslinje", + "column.status": "Toot", "column_back_button.label": "Tillbaka", "column_header.hide_settings": "Dölj inställningar", "column_header.moveLeft_settings": "Flytta kolumnen till vänster", diff --git a/app/javascript/mastodon/locales/ta.json b/app/javascript/mastodon/locales/ta.json index 3266102b1d..3caf301d08 100644 --- a/app/javascript/mastodon/locales/ta.json +++ b/app/javascript/mastodon/locales/ta.json @@ -63,6 +63,7 @@ "column.notifications": "Notifications", "column.pins": "Pinned toot", "column.public": "கூட்டாட்சி காலக்கெடு", + "column.status": "Toot", "column_back_button.label": "ஆதரி", "column_header.hide_settings": "அமைப்புகளை மறை", "column_header.moveLeft_settings": "நெடுவரிசையை இடதுபுறமாக நகர்த்தவும்", diff --git a/app/javascript/mastodon/locales/te.json b/app/javascript/mastodon/locales/te.json index ee7293aa7c..5827dbb3a8 100644 --- a/app/javascript/mastodon/locales/te.json +++ b/app/javascript/mastodon/locales/te.json @@ -63,6 +63,7 @@ "column.notifications": "ప్రకటనలు", "column.pins": "Pinned toot", "column.public": "సమాఖ్య కాలక్రమం", + "column.status": "Toot", "column_back_button.label": "వెనక్కి", "column_header.hide_settings": "అమర్పులను దాచిపెట్టు", "column_header.moveLeft_settings": "నిలువు వరుసను ఎడమకి తరలించు", diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json index 3ff56f947c..33eb315f1e 100644 --- a/app/javascript/mastodon/locales/th.json +++ b/app/javascript/mastodon/locales/th.json @@ -16,7 +16,7 @@ "account.follows.empty": "ผู้ใช้นี้ยังไม่ได้ติดตามใคร", "account.follows_you": "ติดตามคุณ", "account.hide_reblogs": "ซ่อนการดันจาก @{name}", - "account.last_status": "Last active", + "account.last_status": "ใช้งานล่าสุด", "account.link_verified_on": "ตรวจสอบความเป็นเจ้าของของลิงก์นี้เมื่อ {date}", "account.locked_info": "มีการตั้งสถานะความเป็นส่วนตัวของบัญชีนี้เป็นล็อคอยู่ เจ้าของตรวจทานผู้ที่สามารถติดตามเขาด้วยตนเอง", "account.media": "สื่อ", @@ -25,7 +25,7 @@ "account.mute": "ปิดเสียง @{name}", "account.mute_notifications": "ปิดเสียงการแจ้งเตือนจาก @{name}", "account.muted": "ปิดเสียงอยู่", - "account.never_active": "Never", + "account.never_active": "ไม่เลย", "account.posts": "โพสต์", "account.posts_with_replies": "โพสต์และการตอบกลับ", "account.report": "รายงาน @{name}", @@ -53,7 +53,7 @@ "column.blocks": "ผู้ใช้ที่ปิดกั้นอยู่", "column.community": "เส้นเวลาในเว็บ", "column.direct": "ข้อความโดยตรง", - "column.directory": "Browse profiles", + "column.directory": "เรียกดูโปรไฟล์", "column.domain_blocks": "โดเมนที่ซ่อนอยู่", "column.favourites": "รายการโปรด", "column.follow_requests": "คำขอติดตาม", @@ -63,6 +63,7 @@ "column.notifications": "การแจ้งเตือน", "column.pins": "โพสต์ที่ปักหมุด", "column.public": "เส้นเวลาที่ติดต่อกับภายนอก", + "column.status": "Toot", "column_back_button.label": "ย้อนกลับ", "column_header.hide_settings": "ซ่อนการตั้งค่า", "column_header.moveLeft_settings": "ย้ายคอลัมน์ไปทางซ้าย", @@ -100,8 +101,8 @@ "confirmations.delete_list.message": "คุณแน่ใจหรือไม่ว่าต้องการลบรายการนี้อย่างถาวร?", "confirmations.domain_block.confirm": "ซ่อนทั้งโดเมน", "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.", - "confirmations.logout.confirm": "Log out", - "confirmations.logout.message": "Are you sure you want to log out?", + "confirmations.logout.confirm": "ออกจากระบบ", + "confirmations.logout.message": "คุณแน่ใจหรือไม่ว่าต้องการออกจากระบบ?", "confirmations.mute.confirm": "ปิดเสียง", "confirmations.mute.message": "คุณแน่ใจหรือไม่ว่าต้องการปิดเสียง {name}?", "confirmations.redraft.confirm": "ลบแล้วร่างใหม่", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index ec9bd0f8f9..2c4d820de5 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -63,6 +63,7 @@ "column.notifications": "Bildirimler", "column.pins": "Sabitlenmiş gönderi", "column.public": "Federe zaman tüneli", + "column.status": "Toot", "column_back_button.label": "Geri", "column_header.hide_settings": "Ayarları gizle", "column_header.moveLeft_settings": "Sütunu sola taşı", diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json index 605ebdc08c..6ccb20fc69 100644 --- a/app/javascript/mastodon/locales/uk.json +++ b/app/javascript/mastodon/locales/uk.json @@ -63,6 +63,7 @@ "column.notifications": "Сповіщення", "column.pins": "Закріплені дмухи", "column.public": "Глобальна стрічка", + "column.status": "Toot", "column_back_button.label": "Назад", "column_header.hide_settings": "Приховати налаштування", "column_header.moveLeft_settings": "Змістити колонку вліво", diff --git a/app/javascript/mastodon/locales/whitelist_pt.json b/app/javascript/mastodon/locales/whitelist_br.json similarity index 100% rename from app/javascript/mastodon/locales/whitelist_pt.json rename to app/javascript/mastodon/locales/whitelist_br.json diff --git a/app/javascript/mastodon/locales/whitelist_nn.json b/app/javascript/mastodon/locales/whitelist_nn.json new file mode 100644 index 0000000000..0d4f101c7a --- /dev/null +++ b/app/javascript/mastodon/locales/whitelist_nn.json @@ -0,0 +1,2 @@ +[ +] diff --git a/app/javascript/mastodon/locales/whitelist_pt-PT.json b/app/javascript/mastodon/locales/whitelist_pt-PT.json new file mode 100644 index 0000000000..0d4f101c7a --- /dev/null +++ b/app/javascript/mastodon/locales/whitelist_pt-PT.json @@ -0,0 +1,2 @@ +[ +] diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 8ab7046c1b..2f0373d93c 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -63,6 +63,7 @@ "column.notifications": "通知", "column.pins": "置顶嘟文", "column.public": "跨站公共时间轴", + "column.status": "Toot", "column_back_button.label": "返回", "column_header.hide_settings": "隐藏设置", "column_header.moveLeft_settings": "将此栏左移", diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json index d63a9dd346..0a42aa47f7 100644 --- a/app/javascript/mastodon/locales/zh-HK.json +++ b/app/javascript/mastodon/locales/zh-HK.json @@ -63,6 +63,7 @@ "column.notifications": "通知", "column.pins": "置頂文章", "column.public": "跨站時間軸", + "column.status": "Toot", "column_back_button.label": "返回", "column_header.hide_settings": "隱藏設定", "column_header.moveLeft_settings": "將欄左移", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index d0b95da8c0..82d7b6db58 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -63,6 +63,7 @@ "column.notifications": "通知", "column.pins": "釘選的嘟文", "column.public": "聯邦時間軸", + "column.status": "Toot", "column_back_button.label": "上一頁", "column_header.hide_settings": "隱藏設定", "column_header.moveLeft_settings": "將欄位向左移動", diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js index 049c70cb41..45d3a5c516 100644 --- a/app/javascript/mastodon/reducers/notifications.js +++ b/app/javascript/mastodon/reducers/notifications.js @@ -35,12 +35,12 @@ const notificationToMap = notification => ImmutableMap({ }); const normalizeNotification = (state, notification, usePendingItems) => { - if (usePendingItems) { - return state.update('pendingItems', list => list.unshift(notificationToMap(notification))); - } - const top = state.get('top'); + if (usePendingItems || !top || !state.get('pendingItems').isEmpty()) { + return state.update('pendingItems', list => list.unshift(notificationToMap(notification))).update('unread', unread => unread + 1); + } + if (!top) { state = state.update('unread', unread => unread + 1); } @@ -54,7 +54,7 @@ const normalizeNotification = (state, notification, usePendingItems) => { }); }; -const expandNormalizedNotifications = (state, notifications, next, usePendingItems) => { +const expandNormalizedNotifications = (state, notifications, next, isLoadingRecent, usePendingItems) => { let items = ImmutableList(); notifications.forEach((n, i) => { @@ -63,6 +63,8 @@ const expandNormalizedNotifications = (state, notifications, next, usePendingIte return state.withMutations(mutable => { if (!items.isEmpty()) { + usePendingItems = isLoadingRecent && (usePendingItems || !mutable.get('top') || !mutable.get('pendingItems').isEmpty()); + mutable.update(usePendingItems ? 'pendingItems' : 'items', list => { const lastIndex = 1 + list.findLastIndex( item => item !== null && (compareId(item.get('id'), items.last().get('id')) > 0 || item.get('id') === items.last().get('id')) @@ -91,7 +93,7 @@ const filterNotifications = (state, accountIds) => { const updateTop = (state, top) => { if (top) { - state = state.set('unread', 0); + state = state.set('unread', state.get('pendingItems').size); } return state.set('top', top); @@ -117,7 +119,7 @@ export default function notifications(state = initialState, action) { case NOTIFICATIONS_UPDATE: return normalizeNotification(state, action.notification, action.usePendingItems); case NOTIFICATIONS_EXPAND_SUCCESS: - return expandNormalizedNotifications(state, action.notifications, action.next, action.usePendingItems); + return expandNormalizedNotifications(state, action.notifications, action.next, action.isLoadingRecent, action.usePendingItems); case ACCOUNT_BLOCK_SUCCESS: return filterNotifications(state, [action.relationship.id]); case ACCOUNT_MUTE_SUCCESS: diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js index 0b036f5fe0..f3ed2fc59a 100644 --- a/app/javascript/mastodon/reducers/timelines.js +++ b/app/javascript/mastodon/reducers/timelines.js @@ -40,6 +40,7 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is if (timeline.endsWith(':pinned')) { mMap.set('items', statuses.map(status => status.get('id'))); } else if (!statuses.isEmpty()) { + usePendingItems = isLoadingRecent && (usePendingItems || !mMap.get('top') || !mMap.get('pendingItems').isEmpty()); mMap.update(usePendingItems ? 'pendingItems' : 'items', ImmutableList(), oldIds => { const newIds = statuses.map(status => status.get('id')); @@ -60,15 +61,16 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is }; const updateTimeline = (state, timeline, status, usePendingItems) => { - if (usePendingItems) { + const top = state.getIn([timeline, 'top']); + + if (usePendingItems || !top || !state.getIn([timeline, 'pendingItems']).isEmpty()) { if (state.getIn([timeline, 'pendingItems'], ImmutableList()).includes(status.get('id')) || state.getIn([timeline, 'items'], ImmutableList()).includes(status.get('id'))) { return state; } - return state.update(timeline, initialTimeline, map => map.update('pendingItems', list => list.unshift(status.get('id')))); + return state.update(timeline, initialTimeline, map => map.update('pendingItems', list => list.unshift(status.get('id'))).update('unread', unread => unread + 1)); } - const top = state.getIn([timeline, 'top']); const ids = state.getIn([timeline, 'items'], ImmutableList()); const includesId = ids.includes(status.get('id')); const unread = state.getIn([timeline, 'unread'], 0); @@ -128,7 +130,7 @@ const filterTimeline = (timeline, state, relationship, statuses) => { const updateTop = (state, timeline, top) => { return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { - if (top) mMap.set('unread', 0); + if (top) mMap.set('unread', mMap.get('pendingItems').size); mMap.set('top', top); })); }; diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js index e49dcaadb8..9418188a7c 100644 --- a/app/javascript/packs/public.js +++ b/app/javascript/packs/public.js @@ -14,10 +14,10 @@ function main() { const React = require('react'); const ReactDOM = require('react-dom'); const Rellax = require('rellax'); - const createHistory = require('history').createBrowserHistory; + const { createBrowserHistory } = require('history'); const scrollToDetailedStatus = () => { - const history = createHistory(); + const history = createBrowserHistory(); const detailedStatuses = document.querySelectorAll('.public-layout .detailed-status'); const location = history.location; diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss index f95313a259..5dc067f0e8 100644 --- a/app/javascript/styles/mastodon/accounts.scss +++ b/app/javascript/styles/mastodon/accounts.scss @@ -224,6 +224,7 @@ } .account__header__fields { + max-width: 100vw; padding: 0; margin: 15px -15px -15px; border: 0 none; diff --git a/app/javascript/styles/mastodon/polls.scss b/app/javascript/styles/mastodon/polls.scss index 8b131dffde..e80220f278 100644 --- a/app/javascript/styles/mastodon/polls.scss +++ b/app/javascript/styles/mastodon/polls.scss @@ -79,6 +79,9 @@ top: -1px; border-radius: 50%; vertical-align: middle; + margin-top: auto; + margin-bottom: auto; + flex: 0 0 18px; &.checkbox { border-radius: 4px; diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index dea7fd43c6..e69193b710 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -408,15 +408,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity end def check_for_spam - spam_check = SpamCheck.new(@status) - - return if spam_check.skip? - - if spam_check.spam? - spam_check.flag! - else - spam_check.remember! - end + SpamCheck.perform(@status) end def forward_for_reply diff --git a/app/lib/activitypub/activity/move.rb b/app/lib/activitypub/activity/move.rb index d7a5f595cc..6c6a2b9678 100644 --- a/app/lib/activitypub/activity/move.rb +++ b/app/lib/activitypub/activity/move.rb @@ -10,10 +10,13 @@ class ActivityPub::Activity::Move < ActivityPub::Activity target_account = ActivityPub::FetchRemoteAccountService.new.call(target_uri) - return if target_account.nil? || !target_account.also_known_as.include?(origin_account.uri) + if target_account.nil? || target_account.suspended? || !target_account.also_known_as.include?(origin_account.uri) + unmark_as_processing! + return + end # In case for some reason we didn't have a redirect for the profile already, set it - origin_account.update(moved_to_account: target_account) if origin_account.moved_to_account_id.nil? + origin_account.update(moved_to_account: target_account) # Initiate a re-follow for each follower origin_account.followers.local.select(:id).find_in_batches do |follower_accounts| @@ -40,4 +43,8 @@ class ActivityPub::Activity::Move < ActivityPub::Activity def mark_as_processing! redis.setex("move_in_progress:#{@account.id}", PROCESSING_COOLDOWN, true) end + + def unmark_as_processing! + redis.del("move_in_progress:#{@account.id}") + end end diff --git a/app/lib/spam_check.rb b/app/lib/spam_check.rb index 0cf1b87903..441697364f 100644 --- a/app/lib/spam_check.rb +++ b/app/lib/spam_check.rb @@ -4,9 +4,25 @@ class SpamCheck include Redisable include ActionView::Helpers::TextHelper + # Threshold over which two Nilsimsa values are considered + # to refer to the same text NILSIMSA_COMPARE_THRESHOLD = 95 - NILSIMSA_MIN_SIZE = 10 - EXPIRE_SET_AFTER = 1.week.seconds + + # Nilsimsa doesn't work well on small inputs, so below + # this size, we check only for exact matches with MD5 + NILSIMSA_MIN_SIZE = 10 + + # How long to keep the trail of digests between updates, + # there is no reason to store it forever + EXPIRE_SET_AFTER = 1.week.seconds + + # How many digests to keep in an account's trail. If it's + # too small, spam could rotate around different message templates + MAX_TRAIL_SIZE = 10 + + # How many detected duplicates to allow through before + # considering the message as spam + THRESHOLD = 5 def initialize(status) @account = status.account @@ -21,9 +37,9 @@ class SpamCheck if insufficient_data? false elsif nilsimsa? - any_other_digest?('nilsimsa') { |_, other_digest| nilsimsa_compare_value(digest, other_digest) >= NILSIMSA_COMPARE_THRESHOLD } + digests_over_threshold?('nilsimsa') { |_, other_digest| nilsimsa_compare_value(digest, other_digest) >= NILSIMSA_COMPARE_THRESHOLD } else - any_other_digest?('md5') { |_, other_digest| other_digest == digest } + digests_over_threshold?('md5') { |_, other_digest| other_digest == digest } end end @@ -38,7 +54,7 @@ class SpamCheck # get the correct status ID back, we have to save it in the string value redis.zadd(redis_key, @status.id, digest_with_algorithm) - redis.zremrangebyrank(redis_key, '0', '-10') + redis.zremrangebyrank(redis_key, 0, -(MAX_TRAIL_SIZE + 1)) redis.expire(redis_key, EXPIRE_SET_AFTER) end @@ -78,6 +94,20 @@ class SpamCheck end end + class << self + def perform(status) + spam_check = new(status) + + return if spam_check.skip? + + if spam_check.spam? + spam_check.flag! + else + spam_check.remember! + end + end + end + private def disabled? @@ -149,14 +179,14 @@ class SpamCheck redis.zrange(redis_key, 0, -1) end - def any_other_digest?(filter_algorithm) - other_digests.any? do |record| + def digests_over_threshold?(filter_algorithm) + other_digests.select do |record| algorithm, other_digest, status_id = record.split(':') next unless algorithm == filter_algorithm yield algorithm, other_digest, status_id - end + end.size >= THRESHOLD end def matching_status_ids diff --git a/app/models/concerns/omniauthable.rb b/app/models/concerns/omniauthable.rb index b9c124841b..9607842227 100644 --- a/app/models/concerns/omniauthable.rb +++ b/app/models/concerns/omniauthable.rb @@ -4,7 +4,7 @@ module Omniauthable extend ActiveSupport::Concern TEMP_EMAIL_PREFIX = 'change@me' - TEMP_EMAIL_REGEX = /\Achange@me/ + TEMP_EMAIL_REGEX = /\A#{TEMP_EMAIL_PREFIX}/.freeze included do devise :omniauthable @@ -28,8 +28,8 @@ module Omniauthable # to prevent the identity being locked with accidentally created accounts. # Note that this may leave zombie accounts (with no associated identity) which # can be cleaned up at a later date. - user = signed_in_resource || identity.user - user = create_for_oauth(auth) if user.nil? + user = signed_in_resource || identity.user + user ||= create_for_oauth(auth) if identity.user.nil? identity.user = user @@ -45,7 +45,18 @@ module Omniauthable # exists, we assign a temporary email and ask the user to verify it on # the next step via Auth::SetupController.show - user = User.new(user_params_from_auth(auth)) + strategy = Devise.omniauth_configs[auth.provider.to_sym].strategy + assume_verified = strategy&.security&.assume_email_is_verified + email_is_verified = auth.info.verified || auth.info.verified_email || assume_verified + email = auth.info.verified_email || auth.info.email + email = nil unless email_is_verified + + user = User.find_by(email: email) if email_is_verified + + return user unless user.nil? + + user = User.new(user_params_from_auth(email, auth)) + user.account.avatar_remote_url = auth.info.image if auth.info.image =~ /\A#{URI.regexp(%w(http https))}\z/ user.skip_confirmation! user.save! @@ -54,14 +65,7 @@ module Omniauthable private - def user_params_from_auth(auth) - strategy = Devise.omniauth_configs[auth.provider.to_sym].strategy - assume_verified = strategy.try(:security).try(:assume_email_is_verified) - email_is_verified = auth.info.verified || auth.info.verified_email || assume_verified - email = auth.info.verified_email || auth.info.email - email = email_is_verified && !User.exists?(email: auth.info.email) && email - display_name = auth.info.full_name || [auth.info.first_name, auth.info.last_name].join(' ') - + def user_params_from_auth(email, auth) { email: email || "#{TEMP_EMAIL_PREFIX}-#{auth.uid}-#{auth.provider}.com", password: Devise.friendly_token[0, 20], @@ -69,7 +73,7 @@ module Omniauthable external: true, account_attributes: { username: ensure_unique_username(auth.uid), - display_name: display_name, + display_name: auth.info.full_name || [auth.info.first_name, auth.info.last_name].join(' '), }, } end diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb index edb1bec752..0dacaf654b 100644 --- a/app/models/custom_emoji.rb +++ b/app/models/custom_emoji.rb @@ -63,7 +63,7 @@ class CustomEmoji < ApplicationRecord def copy! copy = self.class.find_or_initialize_by(domain: nil, shortcode: shortcode) copy.image = image - copy.save! + copy.tap(&:save!) end class << self diff --git a/app/models/form/delete_confirmation.rb b/app/models/form/delete_confirmation.rb index 0884a09b88..99d04b331a 100644 --- a/app/models/form/delete_confirmation.rb +++ b/app/models/form/delete_confirmation.rb @@ -3,5 +3,5 @@ class Form::DeleteConfirmation include ActiveModel::Model - attr_accessor :password + attr_accessor :password, :username end diff --git a/app/models/form/two_factor_confirmation.rb b/app/models/form/two_factor_confirmation.rb index b8cf76d058..27ada65333 100644 --- a/app/models/form/two_factor_confirmation.rb +++ b/app/models/form/two_factor_confirmation.rb @@ -3,5 +3,5 @@ class Form::TwoFactorConfirmation include ActiveModel::Model - attr_accessor :code + attr_accessor :otp_attempt end diff --git a/app/models/tag.rb b/app/models/tag.rb index a6aed0d68e..b52b9bc9fa 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -20,7 +20,7 @@ class Tag < ApplicationRecord has_and_belongs_to_many :statuses has_and_belongs_to_many :accounts - has_and_belongs_to_many :sample_accounts, -> { searchable.discoverable.popular.limit(3) }, class_name: 'Account' + has_and_belongs_to_many :sample_accounts, -> { local.discoverable.popular.limit(3) }, class_name: 'Account' has_many :featured_tags, dependent: :destroy, inverse_of: :tag has_one :account_tag_stat, dependent: :destroy @@ -39,6 +39,7 @@ class Tag < ApplicationRecord scope :listable, -> { where(listable: [true, nil]) } scope :discoverable, -> { listable.joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).order(Arel.sql('account_tag_stats.accounts_count desc')) } scope :most_used, ->(account) { joins(:statuses).where(statuses: { account: account }).group(:id).order(Arel.sql('count(*) desc')) } + scope :matches_name, ->(value) { where(arel_table[:name].matches("#{value}%")) } delegate :accounts_count, :accounts_count=, diff --git a/app/models/tag_filter.rb b/app/models/tag_filter.rb new file mode 100644 index 0000000000..8921e186b0 --- /dev/null +++ b/app/models/tag_filter.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +class TagFilter + attr_reader :params + + def initialize(params) + @params = params + end + + def results + scope = Tag.unscoped + + params.each do |key, value| + next if key.to_s == 'page' + + scope.merge!(scope_for(key, value.to_s.strip)) if value.present? + end + + scope.order(id: :desc) + end + + private + + def scope_for(key, value) + case key.to_s + when 'directory' + Tag.discoverable + when 'reviewed' + Tag.reviewed.order(reviewed_at: :desc) + when 'unreviewed' + Tag.unreviewed + when 'pending_review' + Tag.pending_review.order(requested_review_at: :desc) + when 'popular' + Tag.order('max_score DESC NULLS LAST') + when 'active' + Tag.order('last_status_at DESC NULLS LAST') + when 'name' + Tag.matches_name(value) + else + raise "Unknown filter: #{key}" + end + end +end diff --git a/app/presenters/instance_presenter.rb b/app/presenters/instance_presenter.rb index 534752932a..d8a21fa7cd 100644 --- a/app/presenters/instance_presenter.rb +++ b/app/presenters/instance_presenter.rb @@ -33,7 +33,7 @@ class InstancePresenter end def sample_accounts - Rails.cache.fetch('sample_accounts', expires_in: 12.hours) { Account.discoverable.popular.limit(3) } + Rails.cache.fetch('sample_accounts', expires_in: 12.hours) { Account.local.discoverable.popular.limit(3) } end def version_number diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb index a374206eb8..19de377174 100644 --- a/app/services/process_mentions_service.rb +++ b/app/services/process_mentions_service.rb @@ -33,6 +33,7 @@ class ProcessMentionsService < BaseService end status.save! + check_for_spam(status) mentions.each { |mention| create_notification(mention) } end @@ -61,4 +62,8 @@ class ProcessMentionsService < BaseService def resolve_account_service ResolveAccountService.new end + + def check_for_spam(status) + SpamCheck.perform(status) + end end diff --git a/app/services/unfollow_service.rb b/app/services/unfollow_service.rb index b7033d7ebe..151f3674fd 100644 --- a/app/services/unfollow_service.rb +++ b/app/services/unfollow_service.rb @@ -6,9 +6,12 @@ class UnfollowService < BaseService # Unfollow and notify the remote user # @param [Account] source_account Where to unfollow from # @param [Account] target_account Which to unfollow - def call(source_account, target_account) + # @param [Hash] options + # @option [Boolean] :skip_unmerge + def call(source_account, target_account, options = {}) @source_account = source_account @target_account = target_account + @options = options unfollow! || undo_follow_request! end @@ -21,9 +24,11 @@ class UnfollowService < BaseService return unless follow follow.destroy! + create_notification(follow) if !@target_account.local? && @target_account.activitypub? create_reject_notification(follow) if @target_account.local? && !@source_account.local? && @source_account.activitypub? - UnmergeWorker.perform_async(@target_account.id, @source_account.id) + UnmergeWorker.perform_async(@target_account.id, @source_account.id) unless @options[:skip_unmerge] + follow end @@ -33,7 +38,9 @@ class UnfollowService < BaseService return unless follow_request follow_request.destroy! + create_notification(follow_request) unless @target_account.local? + follow_request end diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index af7a59802e..06f29b79b7 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -4,35 +4,35 @@ .dashboard__counters %div = link_to admin_accounts_url(local: 1, recent: 1) do - .dashboard__counters__num= number_with_delimiter @users_count + .dashboard__counters__num= number_to_human @users_count, strip_insignificant_zeros: true .dashboard__counters__label= t 'admin.dashboard.total_users' %div %div - .dashboard__counters__num= number_with_delimiter @registrations_week + .dashboard__counters__num= number_to_human @registrations_week, strip_insignificant_zeros: true .dashboard__counters__label= t 'admin.dashboard.week_users_new' %div %div - .dashboard__counters__num= number_with_delimiter @logins_week + .dashboard__counters__num= number_to_human @logins_week, strip_insignificant_zeros: true .dashboard__counters__label= t 'admin.dashboard.week_users_active' %div = link_to admin_pending_accounts_path do - .dashboard__counters__num= number_with_delimiter @pending_users_count + .dashboard__counters__num= number_to_human @pending_users_count, strip_insignificant_zeros: true .dashboard__counters__label= t 'admin.dashboard.pending_users' %div = link_to admin_reports_url do - .dashboard__counters__num= number_with_delimiter @reports_count + .dashboard__counters__num= number_to_human @reports_count, strip_insignificant_zeros: true .dashboard__counters__label= t 'admin.dashboard.open_reports' %div - = link_to admin_tags_path(review: 'pending_review') do - .dashboard__counters__num= number_with_delimiter @pending_tags_count + = link_to admin_tags_path(pending_review: '1') do + .dashboard__counters__num= number_to_human @pending_tags_count, strip_insignificant_zeros: true .dashboard__counters__label= t 'admin.dashboard.pending_tags' %div %div - .dashboard__counters__num= number_with_delimiter @interactions_week + .dashboard__counters__num= number_to_human @interactions_week, strip_insignificant_zeros: true .dashboard__counters__label= t 'admin.dashboard.week_interactions' %div = link_to sidekiq_url do - .dashboard__counters__num= number_with_delimiter @queue_backlog + .dashboard__counters__num= number_to_human @queue_backlog, strip_insignificant_zeros: true .dashboard__counters__label= t 'admin.dashboard.backlog' .dashboard__widgets diff --git a/app/views/admin/tags/index.html.haml b/app/views/admin/tags/index.html.haml index c9af7c14d3..ef05a9bd67 100644 --- a/app/views/admin/tags/index.html.haml +++ b/app/views/admin/tags/index.html.haml @@ -5,16 +5,36 @@ .filter-subset %strong= t('admin.tags.context') %ul - %li= filter_link_to t('generic.all'), context: nil - %li= filter_link_to t('admin.tags.directory'), context: 'directory' + %li= filter_link_to t('generic.all'), directory: nil + %li= filter_link_to t('admin.tags.directory'), directory: '1' .filter-subset %strong= t('admin.tags.review') %ul - %li= filter_link_to t('generic.all'), review: nil - %li= filter_link_to t('admin.tags.unreviewed'), review: 'unreviewed' - %li= filter_link_to t('admin.tags.reviewed'), review: 'reviewed' - %li= filter_link_to safe_join([t('admin.accounts.moderation.pending'), "(#{Tag.pending_review.count})"], ' '), review: 'pending_review' + %li= filter_link_to t('generic.all'), reviewed: nil, unreviewed: nil, pending_review: nil + %li= filter_link_to t('admin.tags.unreviewed'), unreviewed: '1', reviewed: nil, pending_review: nil + %li= filter_link_to t('admin.tags.reviewed'), reviewed: '1', unreviewed: nil, pending_review: nil + %li= filter_link_to safe_join([t('admin.accounts.moderation.pending'), "(#{Tag.pending_review.count})"], ' '), pending_review: '1', reviewed: nil, unreviewed: nil + + .filter-subset + %strong= t('generic.order_by') + %ul + %li= filter_link_to t('admin.tags.most_recent'), popular: nil, active: nil + %li= filter_link_to t('admin.tags.most_popular'), popular: '1', active: nil + %li= filter_link_to t('admin.tags.last_active'), active: '1', popular: nil + += form_tag admin_tags_url, method: 'GET', class: 'simple_form' do + .fields-group + - Admin::FilterHelper::TAGS_FILTERS.each do |key| + = hidden_field_tag key, params[key] if params[key].present? + + - %i(name).each do |key| + .input.string.optional + = text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.tags.#{key}") + + .actions + %button= t('admin.accounts.search') + = link_to t('admin.accounts.reset'), admin_tags_path, class: 'button negative' %hr.spacer/ @@ -43,7 +63,7 @@ = paginate @tags -- if params[:review] == 'pending_review' +- if params[:pending_review] == '1' %hr.spacer/ %div{ style: 'overflow: hidden' } diff --git a/app/views/admin/tags/show.html.haml b/app/views/admin/tags/show.html.haml index d54a43c1e8..1d970d637f 100644 --- a/app/views/admin/tags/show.html.haml +++ b/app/views/admin/tags/show.html.haml @@ -3,7 +3,7 @@ .dashboard__counters %div - = link_to web_url("timelines/tag/#{@tag.name}") do + = link_to tag_url(@tag), target: '_blank', rel: 'noopener' do .dashboard__counters__num= number_with_delimiter @accounts_today .dashboard__counters__label= t 'admin.tags.accounts_today' %div diff --git a/app/views/admin_mailer/new_trending_tag.text.erb b/app/views/admin_mailer/new_trending_tag.text.erb index f3087df375..e4bfdc5912 100644 --- a/app/views/admin_mailer/new_trending_tag.text.erb +++ b/app/views/admin_mailer/new_trending_tag.text.erb @@ -2,4 +2,4 @@ <%= raw t('admin_mailer.new_trending_tag.body', name: @tag.name) %> -<%= raw t('application_mailer.view')%> <%= admin_tags_url(review: 'pending_review') %> +<%= raw t('application_mailer.view')%> <%= admin_tags_url(pending_review: '1') %> diff --git a/app/views/settings/deletes/show.html.haml b/app/views/settings/deletes/show.html.haml index 6e2ff31c57..08792e0afd 100644 --- a/app/views/settings/deletes/show.html.haml +++ b/app/views/settings/deletes/show.html.haml @@ -20,7 +20,10 @@ %hr.spacer/ - = f.input :password, wrapper: :with_block_label, input_html: { :autocomplete => 'off' }, hint: t('deletes.confirm_password') + - if current_user.encrypted_password.present? + = f.input :password, wrapper: :with_block_label, input_html: { :autocomplete => 'off' }, hint: t('deletes.confirm_password') + - else + = f.input :username, wrapper: :with_block_label, input_html: { :autocomplete => 'off' }, hint: t('deletes.confirm_username') .actions = f.button :button, t('deletes.proceed'), type: :submit, class: 'negative' diff --git a/app/views/settings/two_factor_authentication/confirmations/new.html.haml b/app/views/settings/two_factor_authentication/confirmations/new.html.haml index e641552991..86cf1f695e 100644 --- a/app/views/settings/two_factor_authentication/confirmations/new.html.haml +++ b/app/views/settings/two_factor_authentication/confirmations/new.html.haml @@ -12,7 +12,7 @@ %samp.qr-alternative__code= current_user.otp_secret.scan(/.{4}/).join(' ') .fields-group - = f.input :code, wrapper: :with_label, hint: t('two_factor_authentication.code_hint'), label: t('simple_form.labels.defaults.otp_attempt'), input_html: { :autocomplete => 'off' }, required: true + = f.input :otp_attempt, wrapper: :with_label, hint: t('two_factor_authentication.code_hint'), label: t('simple_form.labels.defaults.otp_attempt'), input_html: { :autocomplete => 'off' }, required: true .actions = f.button :button, t('two_factor_authentication.enable'), type: :submit diff --git a/app/views/settings/two_factor_authentications/show.html.haml b/app/views/settings/two_factor_authentications/show.html.haml index 259bcd1ef3..93509e022f 100644 --- a/app/views/settings/two_factor_authentications/show.html.haml +++ b/app/views/settings/two_factor_authentications/show.html.haml @@ -10,7 +10,7 @@ %hr/ = simple_form_for @confirmation, url: settings_two_factor_authentication_path, method: :delete do |f| - = f.input :code, wrapper: :with_label, hint: t('two_factor_authentication.code_hint'), label: t('simple_form.labels.defaults.otp_attempt'), input_html: { :autocomplete => 'off' }, required: true + = f.input :otp_attempt, wrapper: :with_label, hint: t('two_factor_authentication.code_hint'), label: t('simple_form.labels.defaults.otp_attempt'), input_html: { :autocomplete => 'off' }, required: true .actions = f.button :button, t('two_factor_authentication.disable'), type: :submit diff --git a/app/workers/scheduler/ip_cleanup_scheduler.rb b/app/workers/scheduler/ip_cleanup_scheduler.rb index 42620332e7..4f44078d8e 100644 --- a/app/workers/scheduler/ip_cleanup_scheduler.rb +++ b/app/workers/scheduler/ip_cleanup_scheduler.rb @@ -9,7 +9,7 @@ class Scheduler::IpCleanupScheduler def perform time_ago = RETENTION_PERIOD.ago - SessionActivation.where('updated_at < ?', time_ago).destroy_all - User.where('last_sign_in_at < ?', time_ago).update_all(last_sign_in_ip: nil) + SessionActivation.where('updated_at < ?', time_ago).in_batches.destroy_all + User.where('last_sign_in_at < ?', time_ago).where.not(last_sign_in_ip: nil).in_batches.update_all(last_sign_in_ip: nil) end end diff --git a/app/workers/unfollow_follow_worker.rb b/app/workers/unfollow_follow_worker.rb index 50d3bf034a..95549e107a 100644 --- a/app/workers/unfollow_follow_worker.rb +++ b/app/workers/unfollow_follow_worker.rb @@ -11,7 +11,7 @@ class UnfollowFollowWorker new_target_account = Account.find(new_target_account_id) FollowService.new.call(follower_account, new_target_account) - UnfollowService.new.call(follower_account, old_target_account) + UnfollowService.new.call(follower_account, old_target_account, skip_unmerge: true) rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError true end diff --git a/app/workers/web/push_notification_worker.rb b/app/workers/web/push_notification_worker.rb index 9010439758..46aeaa30b1 100644 --- a/app/workers/web/push_notification_worker.rb +++ b/app/workers/web/push_notification_worker.rb @@ -11,7 +11,13 @@ class Web::PushNotificationWorker subscription.push(notification) unless notification.activity.nil? rescue Webpush::ResponseError => e - subscription.destroy! if (400..499).cover?(e.response.code.to_i) + code = e.response.code.to_i + + if (400..499).cover?(code) && ![408, 429].include?(code) + subscription.destroy! + else + raise e + end rescue ActiveRecord::RecordNotFound true end diff --git a/config/application.rb b/config/application.rb index f49deffbb6..5fd37120d0 100644 --- a/config/application.rb +++ b/config/application.rb @@ -76,7 +76,7 @@ module Mastodon :no, :oc, :pl, - :pt, + :'pt-PT', :'pt-BR', :ro, :ru, diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index cd9bacf680..311583820a 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -71,10 +71,13 @@ end Devise.setup do |config| config.warden do |manager| + manager.default_strategies(scope: :user).unshift :database_authenticatable manager.default_strategies(scope: :user).unshift :ldap_authenticatable if Devise.ldap_authentication manager.default_strategies(scope: :user).unshift :pam_authenticatable if Devise.pam_authentication - manager.default_strategies(scope: :user).unshift :two_factor_authenticatable - manager.default_strategies(scope: :user).unshift :two_factor_backupable + + # We handle 2FA in our own sessions controller so this gets in the way + manager.default_strategies(scope: :user).delete :two_factor_backupable + manager.default_strategies(scope: :user).delete :two_factor_authenticatable end # The secret key used by Devise. Devise uses this key to generate diff --git a/config/locales/activerecord.br.yml b/config/locales/activerecord.br.yml new file mode 100644 index 0000000000..c7677c850c --- /dev/null +++ b/config/locales/activerecord.br.yml @@ -0,0 +1 @@ +br: diff --git a/config/locales/activerecord.nn.yml b/config/locales/activerecord.nn.yml new file mode 100644 index 0000000000..777f4e600f --- /dev/null +++ b/config/locales/activerecord.nn.yml @@ -0,0 +1 @@ +nn: diff --git a/config/locales/activerecord.pt.yml b/config/locales/activerecord.pt.yml deleted file mode 100644 index 556fcfc4fd..0000000000 --- a/config/locales/activerecord.pt.yml +++ /dev/null @@ -1,13 +0,0 @@ ---- -pt: - activerecord: - errors: - models: - account: - attributes: - username: - invalid: apenas letras, números e underscores - status: - attributes: - reblog: - taken: do status já existe diff --git a/config/locales/ar.yml b/config/locales/ar.yml index 82d2485a75..45001e6fc2 100644 --- a/config/locales/ar.yml +++ b/config/locales/ar.yml @@ -240,6 +240,7 @@ ar: copied_msg: تم إنشاء نسخة محلية للإيموجي بنجاح copy: نسخ copy_failed_msg: فشلت عملية إنشاء نسخة محلية لهذا الإيموجي + create_new_category: انشئ فئة جديدة created_msg: تم إنشاء الإيموجي بنجاح! delete: حذف destroyed_msg: تمت عملية تدمير الإيموجي بنجاح! @@ -256,6 +257,7 @@ ar: shortcode: الترميز المُصَغّر shortcode_hint: على الأقل حرفين، و فقط رموز أبجدية عددية و أسطر سفلية title: الإيموجي الخاصة + uncategorized: غير مصنّف unlisted: غير مدرج update_failed_msg: تعذرت عملية تحديث ذاك الإيموجي updated_msg: تم تحديث الإيموجي بنجاح! @@ -501,6 +503,7 @@ ar: in_directory: "%{count} في سجل حسابات المستخدمين" title: الوسوم trending_right_now: متداول اللحظة + unique_uses_today: "%{count} منشورات اليوم" unreviewed: غير مُراجَع updated_msg: تم تحديث إعدادات الوسوم بنجاح title: الإدارة @@ -595,7 +598,6 @@ ar: x_months: "%{count} شه" x_seconds: "%{count}ث" deletes: - bad_password_msg: إنّ الكلمة السرية التي أدخلتها غير صحيحة confirm_password: قم بإدخال كلمتك السرية الحالية للتحقق من هويتك proceed: حذف حساب success_msg: تم حذف حسابك بنجاح diff --git a/config/locales/ast.yml b/config/locales/ast.yml index e801d4b517..72b87a6acb 100644 --- a/config/locales/ast.yml +++ b/config/locales/ast.yml @@ -131,7 +131,6 @@ ast: half_a_minute: Púramente agora less_than_x_seconds: Púramente agora deletes: - bad_password_msg: "¡Bon intentu, crackers! Contraseña incorreuta" confirm_password: Introduz la contraseña pa verificar la to identidá errors: '400': The request you submitted was invalid or malformed. diff --git a/config/locales/br.yml b/config/locales/br.yml new file mode 100644 index 0000000000..3710084e7d --- /dev/null +++ b/config/locales/br.yml @@ -0,0 +1,20 @@ +--- +br: + errors: + '400': The request you submitted was invalid or malformed. + '403': You don't have permission to view this page. + '404': The page you are looking for isn't here. + '406': This page is not available in the requested format. + '410': The page you were looking for doesn't exist here anymore. + '422': + '429': Throttled + '500': + '503': The page could not be served due to a temporary server failure. + invites: + expires_in: + '1800': 30 minutes + '21600': 6 hours + '3600': 1 hour + '43200': 12 hours + '604800': 1 week + '86400': 1 day diff --git a/config/locales/ca.yml b/config/locales/ca.yml index a23b6ddf47..bfd7c514db 100644 --- a/config/locales/ca.yml +++ b/config/locales/ca.yml @@ -620,7 +620,6 @@ ca: x_months: "%{count} mesos" x_seconds: "%{count} s" deletes: - bad_password_msg: Bon intent hackers! La contrasenya no és correcta confirm_password: Introdueix la contrasenya actual per a verificar la identitat proceed: Suprimeix el compte success_msg: El compte s'ha eliminat correctament diff --git a/config/locales/co.yml b/config/locales/co.yml index 8ecff0d595..e91b1361f1 100644 --- a/config/locales/co.yml +++ b/config/locales/co.yml @@ -427,6 +427,9 @@ co: custom_css: desc_html: Mudificà l'apparenza cù CSS caricatu nant'à ogni pagina title: CSS persunalizatu + default_noindex: + desc_html: Tocca tutti quelli ch'ùn anu micca cambiatu stu parametru + title: Ritirà l'utilizatori di l'indicazione nant'à i mutori di ricerca domain_blocks: all: À tutti disabled: À nimu @@ -629,7 +632,6 @@ co: x_months: "%{count}Me" x_seconds: "%{count}s" deletes: - bad_password_msg: È nò! Sta chjave ùn hè curretta confirm_password: Entrate a vostra chjave d’accessu attuale per verificà a vostra identità proceed: Sguassà u contu success_msg: U vostru contu hè statu sguassatu diff --git a/config/locales/cs.yml b/config/locales/cs.yml index 46700be56b..569eb35d21 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -233,10 +233,12 @@ cs: deleted_status: "(smazaný toot)" title: Záznam auditu custom_emojis: + assign_category: Přiřadit kategorii by_domain: Doména copied_msg: Místní kopie emoji byla úspěšně vytvořena copy: Kopírovat copy_failed_msg: Nebylo možné vytvořit místní kopii tohoto emoji + create_new_category: Vytvořit novou kategorii created_msg: Emoji úspěšně vytvořeno! delete: Smazat destroyed_msg: Emoji úspěšně zničeno! @@ -253,6 +255,7 @@ cs: shortcode: Zkratka shortcode_hint: Alespoň 2 znaky, pouze alfanumerické znaky a podtržítka title: Vlastní emoji + uncategorized: Nezařazená unlisted: Neuvedeno update_failed_msg: Nebylo možné aktualizovat toto emoji updated_msg: Emoji úspěšně aktualizováno! @@ -436,6 +439,9 @@ cs: custom_css: desc_html: Pozměnit vzhled pomocí šablony CSS načtené na každé stránce title: Vlastní CSS + default_noindex: + desc_html: Ovlivňuje všechny uživatele, kteří toto nastavení sami nezměnili + title: Odhlásit uživatele z indexování vyhledávačemi ve výchozím stavu domain_blocks: all: Všem disabled: Nikomu @@ -447,7 +453,7 @@ cs: desc_html: Zobrazuje se na hlavní stránce. Doporučuje se rozlišení alespoň 600x100 px. Pokud toto není nastaveno, bude zobrazena miniatura serveru title: Hlavní obrázek mascot: - desc_html: Zobrazuje se na hlavní stránce. Doporučuje se rozlišení alespoň 293x205 px. Pokud toto není nastaveno, bude zobrazen výchozí maskot + desc_html: Zobrazuje se na několika stránkách. Doporučuje se rozlišení alespoň 293x205 px. Pokud toto není nastaveno, bude zobrazen výchozí maskot title: Obrázek maskota peers_api_enabled: desc_html: Domény, na které tento server narazil ve fedivesmíru @@ -638,7 +644,6 @@ cs: x_months: "%{count} mesíců" x_seconds: "%{count} s" deletes: - bad_password_msg: Dobrý pokus, hackeři! Nesprávné heslo confirm_password: Zadejte svoje současné heslo pro ověření vaší identity proceed: Odstranit účet success_msg: Váš účet byl úspěšně odstraněn diff --git a/config/locales/cy.yml b/config/locales/cy.yml index a1d637f2e1..a58ea25349 100644 --- a/config/locales/cy.yml +++ b/config/locales/cy.yml @@ -581,7 +581,6 @@ cy: x_months: "%{count}mis" x_seconds: "%{count}eiliad" deletes: - bad_password_msg: Go dda, hacwyr! Cyfrinair anghywir confirm_password: Mewnbynnwch eich cyfrinair presennol i gadarnhau mai chi sydd yno proceed: Dileu cyfrif success_msg: Llwyddwyd i ddileu eich cyfrif diff --git a/config/locales/da.yml b/config/locales/da.yml index 70397c77b1..06a68f6841 100644 --- a/config/locales/da.yml +++ b/config/locales/da.yml @@ -207,6 +207,7 @@ da: copied_msg: Succesfuldt oprettede en lokal kopi af humørikonet copy: Kopier copy_failed_msg: Kunne ikke oprette en lokal kopi af dette humørikon + create_new_category: Opret ny kategori created_msg: Humørikon succesfuldt oprettet! delete: Slet destroyed_msg: Emojo succesfuldt destrueret! @@ -514,7 +515,6 @@ da: over_x_years: "%{count}år" x_months: "%{count}md" deletes: - bad_password_msg: Godt forsøg, hackere! Forkert kodeord confirm_password: Indtast dit nuværende kodeord for at bekræfte din identitet proceed: Slet konto success_msg: Din konto er nu blevet slettet diff --git a/config/locales/de.yml b/config/locales/de.yml index 0af7be2f42..fb988668a5 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -225,10 +225,12 @@ de: deleted_status: "(gelöschter Beitrag)" title: Überprüfungsprotokoll custom_emojis: + assign_category: Kategorie zuweisen by_domain: Domain copied_msg: Eine lokale Kopie des Emojis wurde erstellt copy: Kopieren copy_failed_msg: Es konnte keine lokale Kopie des Emojis erstellt werden + create_new_category: Neue Kategorie erstellen created_msg: Emoji erstellt! delete: Löschen destroyed_msg: Emoji gelöscht! @@ -245,6 +247,7 @@ de: shortcode: Kürzel shortcode_hint: Mindestens 2 Zeichen, nur Buchstaben, Ziffern und Unterstriche title: Eigene Emojis + uncategorized: Nicht kategorisiert unlisted: Ungelistet update_failed_msg: Konnte dieses Emoji nicht aktualisieren updated_msg: Emoji erfolgreich aktualisiert! @@ -424,6 +427,9 @@ de: custom_css: desc_html: Verändere das Aussehen mit CSS, dass auf jeder Seite geladen wird title: Benutzerdefiniertes CSS + default_noindex: + desc_html: Beeinflusst alle Benutzer, die diese Einstellung nicht selbst geändert haben + title: Benutzer aus Suchmaschinen-Indizierung standardmäßig herausnehmen domain_blocks: all: An alle disabled: An niemanden @@ -626,7 +632,6 @@ de: x_months: "%{count}mo" x_seconds: "%{count}s" deletes: - bad_password_msg: Falsches Passwort confirm_password: Gib dein derzeitiges Passwort ein, um deine Identität zu bestätigen proceed: Konto löschen success_msg: Dein Konto wurde erfolgreich gelöscht @@ -727,6 +732,7 @@ de: all: Alle changes_saved_msg: Änderungen gespeichert! copy: Kopieren + no_batch_actions_available: Keine Massenaktionen auf dieser Seite verfügbar order_by: Sortieren nach save_changes: Änderungen speichern validation_errors: diff --git a/config/locales/devise.br.yml b/config/locales/devise.br.yml new file mode 100644 index 0000000000..c7677c850c --- /dev/null +++ b/config/locales/devise.br.yml @@ -0,0 +1 @@ +br: diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index 2930733c00..5defa66245 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -21,7 +21,7 @@ en: action: Verify email address action_with_app: Confirm and return to %{app} explanation: You have created an account on %{host} with this email address. You are one click away from activating it. If this wasn't you, please ignore this email. - explanation_when_pending: You applied for an invite to %{host} with this email address. Once you confirm your e-mail address, we will review your application. You can't login until then. If your application is rejected, your data will be removed, so no further action will be required from you. If this wasn't you, please ignore this email. + explanation_when_pending: You applied for an invite to %{host} with this email address. Once you confirm your e-mail address, we will review your application. You can login to change your details or delete your account, but you cannot access most of the functions until your account is approved. If your application is rejected, your data will be removed, so no further action will be required from you. If this wasn't you, please ignore this email. extra_html: Please also check out the rules of the server and our terms of service. subject: 'Mastodon: Confirmation instructions for %{instance}' title: Verify email address diff --git a/config/locales/devise.nn.yml b/config/locales/devise.nn.yml new file mode 100644 index 0000000000..777f4e600f --- /dev/null +++ b/config/locales/devise.nn.yml @@ -0,0 +1 @@ +nn: diff --git a/config/locales/devise.pt.yml b/config/locales/devise.pt.yml deleted file mode 100644 index 9b44bbf00c..0000000000 --- a/config/locales/devise.pt.yml +++ /dev/null @@ -1,83 +0,0 @@ ---- -pt: - devise: - confirmations: - confirmed: O teu endereço de e-mail foi confirmado com sucesso. - send_instructions: Vais receber um email com as instruções para confirmar o teu endereço de email dentro de alguns minutos. Por favor, verifica a caixa de spam se não recebeste o e-mail. - send_paranoid_instructions: Se o teu endereço de email já existir na nossa base de dados, vais receber um email com as instruções de confirmação dentro de alguns minutos. Por favor, verifica a caixa de spam se não recebeste o e-mail. - failure: - already_authenticated: A tua sessão já está aberta. - inactive: A tua conta ainda não está ativada. - invalid: "%{authentication_keys} ou palavra-passe inválida." - last_attempt: Tens mais uma tentativa antes de a tua conta ficar bloqueada. - locked: A tua conta está bloqueada. - not_found_in_database: "%{authentication_keys} ou palavra-passe inválida." - timeout: A tua sessão expirou. Por favor, entra de novo para continuares. - unauthenticated: Precisas de entrar na tua conta ou de te registares antes de continuar. - unconfirmed: Tens de confirmar o teu endereço de email antes de continuar. - mailer: - confirmation_instructions: - action: Verificar o endereço de e-mail - action_with_app: Confirmar e regressar a %{app} - explanation: Criaste uma conta em %{host} com este endereço de e-mail. Estás a um clique de activá-la. Se não foste tu que fizeste este registo, por favor ignora esta mensagem. - extra_html: Por favor lê as regras da instância e os nossos termos de serviço. - subject: 'Mastodon: Instruções de confirmação %{instance}' - title: Verificar o endereço de e-mail - email_changed: - explanation: 'O e-mail associado à tua conta será alterado para:' - extra: Se não alteraste o teu e-mail é possível que alguém tenha conseguido aceder à tua conta. Por favor muda a tua palavra-passe imediatamente ou entra em contato com um administrador do servidor se ficaste sem acesso à tua conta. - subject: 'Mastodon: Email alterado' - title: Novo endereço de e-mail - password_change: - explanation: A palavra-passe da tua conta foi alterada. - extra: Se não alteraste a tua palavra-passe, é possível que alguém tenha conseguido aceder à tua conta. Por favor muda a tua palavra-passe imediatamente ou entra em contato com um administrador do servidor se ficaste sem acesso à tua conta. - subject: 'Mastodon: Nova palavra-passe' - title: Palavra-passe alterada - reconfirmation_instructions: - explanation: Confirma o teu novo endereço para alterar o e-mail. - extra: Se esta mudança não foi iniciada por ti, por favor ignora este e-mail. O endereço de e-mail para a tua conta do Mastodon não irá mudar enquanto não acederes ao link acima. - subject: 'Mastodon: Confirmação de e-mail %{instance}' - title: Validar o endereço de e-mail - reset_password_instructions: - action: Alterar palavra-passe - explanation: Pediste a alteração da palavra-passe da tua conta. - extra: Se não fizeste este pedido, por favor ignora este e-mail. A tua palavra-passe não irá mudar se não acederes ao link acima e criares uma nova. - subject: 'Mastodon: Instruções para alterar a palavra-passe' - title: Solicitar nova palavra-passe - unlock_instructions: - subject: 'Mastodon: Instruções para desbloquear a tua conta' - omniauth_callbacks: - failure: Não foi possível autenticar %{kind} porque "%{reason}". - success: Autenticado com sucesso na conta %{kind}. - passwords: - no_token: Não pode aceder a esta página se não vier através do link enviado por email para alteração da sua palavra-passe. Se usaste esse link para chegar aqui, por favor verifica que o endereço URL actual é o mesmo do que foi enviado no email. - send_instructions: Vais receber um email com instruções para alterar a palavra-passe dentro de algns minutos. - send_paranoid_instructions: Se o teu endereço de email existe na nossa base de dados, vais receber um link para recuperar a palavra-passe dentro de alguns minutos. - updated: A tua palavra-passe foi alterada. Estás agora autenticado na tua conta. - updated_not_active: A tua palavra-passe foi alterada. - registrations: - destroyed: Adeus! A tua conta foi cancelada. Esperamos ver-te em breve. - signed_up: Bem-vindo! A tua conta foi registada com sucesso. - signed_up_but_inactive: A tua conta foi registada. No entanto ainda não está activa. - signed_up_but_locked: A tua conta foi registada. No entanto está bloqueada. - signed_up_but_unconfirmed: Uma mensagem com um link de confirmação foi enviada para o teu email. Por favor segue esse link para activar a tua conta. - update_needs_confirmation: Alteraste o teu endereço de email ou palavra-passe, mas é necessário confirmar essa alteração. Por favor vai ao teu email e segue link que te enviámos. - updated: A tua conta foi actualizada com sucesso. - sessions: - already_signed_out: Sessão encerrada. - signed_in: Sessão iniciada. - signed_out: Sessão encerrada. - unlocks: - send_instructions: Vais receber um email com instruções para desbloquear a tua conta dentro de alguns minutos. - send_paranoid_instructions: Se a tua conta existe, vais receber um email com instruções a detalhar como a desbloquear dentro de alguns minutos. - unlocked: A sua conta foi desbloqueada. Por favor inica uma nova sessão para continuar. - errors: - messages: - already_confirmed: já confirmado, por favor tente iniciar sessão - confirmation_period_expired: tem de ser confirmado durante %{period}, por favor tenta outra vez - expired: expirou, por favor tente outra vez - not_found: não encontrado - not_locked: não estava bloqueada - not_saved: - one: '1 erro impediu este %{resource} de ser guardado:' - other: "%{count} erros impediram este %{resource} de ser guardado:" diff --git a/config/locales/doorkeeper.br.yml b/config/locales/doorkeeper.br.yml new file mode 100644 index 0000000000..c7677c850c --- /dev/null +++ b/config/locales/doorkeeper.br.yml @@ -0,0 +1 @@ +br: diff --git a/config/locales/doorkeeper.nn.yml b/config/locales/doorkeeper.nn.yml new file mode 100644 index 0000000000..777f4e600f --- /dev/null +++ b/config/locales/doorkeeper.nn.yml @@ -0,0 +1 @@ +nn: diff --git a/config/locales/doorkeeper.pt.yml b/config/locales/doorkeeper.pt-PT.yml similarity index 99% rename from config/locales/doorkeeper.pt.yml rename to config/locales/doorkeeper.pt-PT.yml index f21e84d17f..42068e0a0b 100644 --- a/config/locales/doorkeeper.pt.yml +++ b/config/locales/doorkeeper.pt-PT.yml @@ -1,5 +1,5 @@ --- -pt: +pt-PT: activerecord: attributes: doorkeeper/application: diff --git a/config/locales/el.yml b/config/locales/el.yml index 43fec340a1..acc97d37e7 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -626,7 +626,6 @@ el: x_months: "%{count}μ" x_seconds: "%{count}δ" deletes: - bad_password_msg: Καλή προσπάθεια χάκερς! Λάθος συνθηματικό confirm_password: Γράψε το τρέχον συνθηματικό σου για να πιστοποιήσεις την ταυτότητά σου proceed: Διαγραφή λογαριασμού success_msg: Ο λογαριασμός σου διαγράφηκε με επιτυχία diff --git a/config/locales/en.yml b/config/locales/en.yml index be8c5eec08..c097a7e39e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -534,6 +534,10 @@ en: context: Context directory: In directory in_directory: "%{count} in directory" + last_active: Last active + most_popular: Most popular + most_recent: Most recent + name: Hashtag review: Review status reviewed: Reviewed title: Hashtags @@ -645,8 +649,9 @@ en: x_months: "%{count}mo" x_seconds: "%{count}s" deletes: - bad_password_msg: The password you entered was incorrect + challenge_not_passed: The information you entered was not correct confirm_password: Enter your current password to verify your identity + confirm_username: Enter your username to confirm the procedure proceed: Delete account success_msg: Your account was successfully deleted warning: diff --git a/config/locales/eo.yml b/config/locales/eo.yml index 5785f9b20f..b5b8656a45 100644 --- a/config/locales/eo.yml +++ b/config/locales/eo.yml @@ -597,7 +597,6 @@ eo: x_months: "%{count}mo" x_seconds: "%{count}s" deletes: - bad_password_msg: Malĝusta pasvorto confirm_password: Enmetu vian nunan pasvorton por konfirmi vian identecon proceed: Forigi konton success_msg: Via konto estis sukcese forigita diff --git a/config/locales/es.yml b/config/locales/es.yml index 184f0da0e5..892d82e9ca 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -56,6 +56,7 @@ es: media: Multimedia moved_html: "%{name} se ha trasladado a %{new_profile_link}:" network_hidden: Esta información no está disponible + never_active: Nunca nothing_here: "¡No hay nada aquí!" people_followed_by: Usuarios a quien %{name} sigue people_who_follow: Usuarios que siguen a %{name} @@ -222,6 +223,7 @@ es: deleted_status: "(estado borrado)" title: Log de auditoría custom_emojis: + assign_category: Asignar categoría by_domain: Dominio copied_msg: Copia local del emoji creada con éxito copy: Copiar @@ -242,6 +244,7 @@ es: shortcode: Código de atajo shortcode_hint: Al menos 2 caracteres, solo caracteres alfanuméricos y guiones bajos title: Emojis personalizados + uncategorized: Sin clasificar unlisted: Sin listar update_failed_msg: No se pudo actualizar ese emoji updated_msg: "¡Emoji actualizado con éxito!" @@ -489,6 +492,7 @@ es: delete: Eliminar nsfw_off: Marcar contenido como no sensible nsfw_on: Marcar contenido como sensible + deleted: Eliminado failed_to_execute: Falló al ejecutar media: title: Multimedia @@ -609,7 +613,6 @@ es: x_months: "%{count}m" x_seconds: "%{count}s" deletes: - bad_password_msg: "¡Buen intento, hackers! Contraseña incorrecta" confirm_password: Ingresa tu contraseña actual para demostrar tu identidad proceed: Eliminar cuenta success_msg: Tu cuenta se eliminó con éxito @@ -617,6 +620,8 @@ es: directory: Directorio de perfiles explanation: Descubre usuarios según sus intereses explore_mastodon: Explorar %{title} + domain_blocks: + domain: Dominio domain_validator: invalid_domain: no es un nombre de dominio válido errors: @@ -676,6 +681,7 @@ es: developers: Desarrolladores more: Mas… resources: Recursos + trending_now: Tendencia ahora generic: all: Todos changes_saved_msg: "¡Cambios guardados con éxito!" diff --git a/config/locales/et.yml b/config/locales/et.yml index d02eb24bab..7d07719830 100644 --- a/config/locales/et.yml +++ b/config/locales/et.yml @@ -585,7 +585,6 @@ et: x_months: "%{count}k" x_seconds: "%{count}s" deletes: - bad_password_msg: Hea proov, häkkerid! Vale salasõna confirm_password: Sisesta oma praegune salasõna, et kinnitada oma identiteet proceed: Kustuta konto success_msg: Konto kustutamine õnnestus diff --git a/config/locales/eu.yml b/config/locales/eu.yml index 56271f3c39..2a4d612961 100644 --- a/config/locales/eu.yml +++ b/config/locales/eu.yml @@ -611,7 +611,6 @@ eu: x_months: "%{count} hilabete" x_seconds: "%{count}s" deletes: - bad_password_msg: Saiakera ona hacker! Pasahitz okerra confirm_password: Sartu zure oraingo pasahitza zure identitatea baieztatzeko proceed: Ezabatu kontua success_msg: Zure kontua ongi ezabatu da diff --git a/config/locales/fa.yml b/config/locales/fa.yml index 0b4d046f33..7f316c7848 100644 --- a/config/locales/fa.yml +++ b/config/locales/fa.yml @@ -620,7 +620,6 @@ fa: x_months: "%{count} ماه" x_seconds: "%{count} ثانیه" deletes: - bad_password_msg: هکر گرامی، رمزی که وارد کردید اشتباه است ؛) confirm_password: رمز فعلی خود را وارد کنید تا معلوم شود که خود شمایید proceed: پاک‌کردن حساب success_msg: حساب شما با موفقیت پاک شد diff --git a/config/locales/fi.yml b/config/locales/fi.yml index 5a3a8ad600..3d8fdce3a5 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -393,7 +393,6 @@ fi: x_months: "%{count} kk" x_seconds: "%{count} s" deletes: - bad_password_msg: Hyvä yritys, hakkerit! Väärä salasana confirm_password: Tunnistaudu syöttämällä nykyinen salasanasi proceed: Poista tili success_msg: Tilin poisto onnistui diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 58b1607516..15d6359b43 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -626,7 +626,6 @@ fr: x_months: "%{count} mois" x_seconds: "%{count} s" deletes: - bad_password_msg: Bien essayé ! Mot de passe incorrect confirm_password: Entrez votre mot de passe pour vérifier votre identité proceed: Supprimer compte success_msg: Votre compte a été supprimé avec succès @@ -1079,7 +1078,7 @@ fr:

Dans le cas où nous déciderions de changer notre politique de confidentialité, nous posterons les modifications sur cette page.

-

Ce document est publié sous lincence CC-BY-SA. Il a été mis à jours pour la dernière fois le 7 mars 2018.

+

Ce document est publié sous licence CC-BY-SA. Il a été mis à jour pour la dernière fois le 7 mars 2018.

Originellement adapté de la politique de confidentialité de Discourse.

title: "%{instance} Conditions d’utilisation et politique de confidentialité" diff --git a/config/locales/gl.yml b/config/locales/gl.yml index 0c515a2eca..20f535ad5c 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -629,7 +629,6 @@ gl: x_months: "%{count}mes" x_seconds: "%{count}s" deletes: - bad_password_msg: Bo intento, hackers! Contrasinal incorrecto confirm_password: Introduza o seu contrasinal para verificar a súa identidade proceed: Eliminar conta success_msg: A súa conta eliminouse correctamente diff --git a/config/locales/hu.yml b/config/locales/hu.yml index 7aa75434cb..5d9097d094 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -611,7 +611,6 @@ hu: x_months: "%{count}h" x_seconds: "%{count}mp" deletes: - bad_password_msg: Haha, hekker! Helytelen jelszó confirm_password: Személyazonosságod megerősítéséhez írd be a jelenlegi jelszavad proceed: Felhasználói fiók törlése success_msg: Felhasználói fiókod sikeresen töröltük diff --git a/config/locales/it.yml b/config/locales/it.yml index f62d309dfc..7b3eede09d 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -617,7 +617,6 @@ it: x_months: "%{count} mesi" x_seconds: "%{count} secondi" deletes: - bad_password_msg: Ci avete provato, hacker! Password errata confirm_password: Inserisci la tua password attuale per verificare la tua identità proceed: Cancella l'account success_msg: Il tuo account è stato cancellato diff --git a/config/locales/ja.yml b/config/locales/ja.yml index dde31ff0d5..f39e048ca2 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -616,7 +616,6 @@ ja: x_months: "%{count}月" x_seconds: "%{count}秒" deletes: - bad_password_msg: パスワードが違います confirm_password: 本人確認のため、現在のパスワードを入力してください proceed: アカウントを削除する success_msg: アカウントは正常に削除されました diff --git a/config/locales/ka.yml b/config/locales/ka.yml index b9b9b664ff..93cc8ec5a6 100644 --- a/config/locales/ka.yml +++ b/config/locales/ka.yml @@ -434,7 +434,6 @@ ka: x_months: "%{count}თვე" x_seconds: "%{count}წმ" deletes: - bad_password_msg: კარგად სცადეთ, ჰაკერებო! არასწორი პაროლი confirm_password: იდენტობის დასამოწმებლად შეიყვანეთ მიმდინარე პაროლი proceed: ანგარიშის გაუქმება success_msg: თქვენი ანგარიში წარმატებით გაუქმდა diff --git a/config/locales/kk.yml b/config/locales/kk.yml index 736816425f..3658b2293b 100644 --- a/config/locales/kk.yml +++ b/config/locales/kk.yml @@ -510,7 +510,6 @@ kk: x_months: "%{count}ай" x_seconds: "%{count}сек" deletes: - bad_password_msg: Болмады ма, хакер бала? Құпиясөз қате confirm_password: Қазіргі құпиясөзіңізді жазыңыз proceed: Аккаунт өшіру success_msg: Аккаунтыңыз сәтті өшірілді diff --git a/config/locales/ko.yml b/config/locales/ko.yml index 1c4170d8af..fc9fa7b80a 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -618,7 +618,6 @@ ko: x_months: "%{count}월" x_seconds: "%{count}초" deletes: - bad_password_msg: 비밀번호가 올바르지 않습니다 confirm_password: 본인 확인을 위해 현재 사용 중인 암호를 입력해 주십시오 proceed: 계정 삭제 success_msg: 계정이 성공적으로 삭제되었습니다 diff --git a/config/locales/lt.yml b/config/locales/lt.yml index a5dd5cbf5d..7aed705cb3 100644 --- a/config/locales/lt.yml +++ b/config/locales/lt.yml @@ -490,7 +490,6 @@ lt: x_months: "%{count}mėn" x_seconds: "%{count}sek" deletes: - bad_password_msg: Geras bandymas, programišiau! Neteisingas slaptažodis confirm_password: Kad patvirtintumėte savo tapatybę, įveskite dabartini slaptažodį proceed: Ištrinti paskyrą success_msg: Jūsų paskyra sėkmingai ištrinta diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 25e6c6591f..9298e0ae08 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -600,7 +600,6 @@ nl: x_months: "%{count}ma" x_seconds: "%{count}s" deletes: - bad_password_msg: Goed geprobeerd hackers! Ongeldig wachtwoord confirm_password: Voer jouw huidige wachtwoord in om jouw identiteit te bevestigen proceed: Account verwijderen success_msg: Jouw account is succesvol verwijderd diff --git a/config/locales/nn.yml b/config/locales/nn.yml new file mode 100644 index 0000000000..a1b61d6e7d --- /dev/null +++ b/config/locales/nn.yml @@ -0,0 +1,20 @@ +--- +nn: + errors: + '400': The request you submitted was invalid or malformed. + '403': You don't have permission to view this page. + '404': The page you are looking for isn't here. + '406': This page is not available in the requested format. + '410': The page you were looking for doesn't exist here anymore. + '422': + '429': Throttled + '500': + '503': The page could not be served due to a temporary server failure. + invites: + expires_in: + '1800': 30 minutes + '21600': 6 hours + '3600': 1 hour + '43200': 12 hours + '604800': 1 week + '86400': 1 day diff --git a/config/locales/no.yml b/config/locales/no.yml index 4cf080be91..1d675aef68 100644 --- a/config/locales/no.yml +++ b/config/locales/no.yml @@ -326,7 +326,6 @@ x_months: "%{count} mnd" x_seconds: "%{count} sek" deletes: - bad_password_msg: Godt forsøk, hacker! Feil passord confirm_password: Skriv inn ditt passord for å verifisere din identitet proceed: Slett konto success_msg: Din konto ble slettet diff --git a/config/locales/oc.yml b/config/locales/oc.yml index 65e381b3a2..2884380b8b 100644 --- a/config/locales/oc.yml +++ b/config/locales/oc.yml @@ -580,7 +580,6 @@ oc: x_months: "%{count} meses" x_seconds: "%{count}s" deletes: - bad_password_msg: Ben ensajat pirata ! Senhal incorrècte confirm_password: Picatz vòstre senhal actual per verificar vòstra identitat proceed: Suprimir lo compte success_msg: Compte ben suprimit diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 2e0c650754..8b32e6d226 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -569,7 +569,6 @@ pl: x_months: "%{count} miesięcy" x_seconds: "%{count}s" deletes: - bad_password_msg: Niezła próba, hakerze! Wprowadzono nieprawidłowe hasło confirm_password: Wprowadź aktualne hasło, aby potwierdzić tożsamość proceed: Usuń konto success_msg: Twoje konto zostało pomyślnie usunięte diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index af4d117e0b..9896f888a4 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -526,7 +526,6 @@ pt-BR: x_months: "%{count} meses" x_seconds: "%{count} segundos" deletes: - bad_password_msg: Boa tentativa, hackers! Senha incorreta confirm_password: Insira a sua senha atual para verificar a sua identidade proceed: Excluir conta success_msg: A sua conta foi excluída com sucesso diff --git a/config/locales/pt.yml b/config/locales/pt-PT.yml similarity index 99% rename from config/locales/pt.yml rename to config/locales/pt-PT.yml index eeb158f6c3..25ee570851 100644 --- a/config/locales/pt.yml +++ b/config/locales/pt-PT.yml @@ -1,5 +1,5 @@ --- -pt: +pt-PT: about: about_hashtag_html: Estes são toots públicos marcados com #%{hashtag}. Podes interagir com eles se tiveres uma conta Mastodon. about_mastodon_html: Mastodon é uma rede social baseada em protocolos abertos da web e software livre e gratuito. É descentralizado como e-mail. @@ -499,7 +499,6 @@ pt: x_months: "%{count} meses" x_seconds: "%{count} segundos" deletes: - bad_password_msg: Boa tentativa, hackers! Palavra-passe incorreta confirm_password: Introduz a palavra-passe atual para verificar a tua identidade proceed: Eliminar conta success_msg: A tua conta foi eliminada com sucesso diff --git a/config/locales/ro.yml b/config/locales/ro.yml index d04d0015f7..7deab60213 100644 --- a/config/locales/ro.yml +++ b/config/locales/ro.yml @@ -43,7 +43,6 @@ ro: x_days: "%{count}z" x_months: "%{count}l" deletes: - bad_password_msg: Bună încercare, hackere! Parolă incorectă confirm_password: Introdu parola curentă pentru a-ți verifica identitatea proceed: Șterge contul success_msg: Contul tău a fost șterg. Nu mai poate fi recuperat :D diff --git a/config/locales/ru.yml b/config/locales/ru.yml index d1ed8d1de1..0c12021182 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -273,7 +273,7 @@ ru: space: Использовано места title: Панель управления total_users: всего пользователей - trends: Тренды + trends: Актуальное week_interactions: взаимодействий на этой неделе week_users_active: активно на этой неделе week_users_new: пользователей на этой неделе @@ -517,6 +517,8 @@ ru: subject: Новая жалоба, узел %{instance} (#%{id}) appearance: advanced_web_interface: Многоколоночный интерфейс + confirmation_dialogs: Окна подтверждений + discovery: Обзор sensitive_content: Чувствительное содержимое application_mailer: notification_preferences: Изменить настройки e-mail @@ -583,7 +585,6 @@ ru: x_months: "%{count}мес" x_seconds: "%{count}сек" deletes: - bad_password_msg: Не вышло, хакеры! Неверный пароль confirm_password: Введите текущий пароль для подтверждения Вашей личности proceed: Удалить аккаунт success_msg: Ваш аккаунт был успешно удален diff --git a/config/locales/simple_form.br.yml b/config/locales/simple_form.br.yml new file mode 100644 index 0000000000..c7677c850c --- /dev/null +++ b/config/locales/simple_form.br.yml @@ -0,0 +1 @@ +br: diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 6c315b0ed3..359142064f 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -144,6 +144,8 @@ en: must_be_follower: Block notifications from non-followers must_be_following: Block notifications from people you don't follow must_be_following_dm: Block direct messages from people you don't follow + invite: + comment: Comment invite_request: text: Why do you want to join? notification_emails: diff --git a/config/locales/simple_form.es.yml b/config/locales/simple_form.es.yml index 35cebcad80..898d200d29 100644 --- a/config/locales/simple_form.es.yml +++ b/config/locales/simple_form.es.yml @@ -5,6 +5,7 @@ es: account_warning_preset: text: Puede usar sintaxis de toots, como URLs, hashtags y menciones admin_account_action: + include_statuses: El usuario verá qué toots han causado la acción de moderación o advertencia send_email_notification: El usuario recibirá una explicación de lo que sucedió con respecto a su cuenta text_html: Opcional. Puede usar sintaxis de toots. Puede añadir configuraciones predefinidas de advertencia para ahorrar tiempo type_html: Elige qué hacer con %{acct} @@ -15,6 +16,7 @@ es: bot: Esta cuenta ejecuta principalmente acciones automatizadas y podría no ser monitorizada context: Uno o múltiples contextos en los que debe aplicarse el filtro digest: Solo enviado tras un largo periodo de inactividad y solo si has recibido mensajes personales durante tu ausencia + discoverable: El directorio del perfil es otra forma en la que su cuenta puede llegar a un público más amplio email: Se le enviará un correo de confirmación fields: Puedes tener hasta 4 elementos mostrándose como una tabla en tu perfil header: PNG, GIF o JPG. Máximo %{size}. Será escalado a %{dimensions}px @@ -47,6 +49,8 @@ es: text: Esto nos ayudará a revisar su aplicación sessions: otp: 'Introduce el código de autenticación de dos factores geberado por tu aplicación de teléfono o usa uno de tus códigos de recuperación:' + tag: + name: Sólo se puede cambiar el cajón de las letras, por ejemplo, para que sea más legible user: chosen_languages: Cuando se marca, solo se mostrarán los toots en los idiomas seleccionados en los timelines públicos labels: @@ -57,6 +61,7 @@ es: account_warning_preset: text: Texto predefinido admin_account_action: + include_statuses: Incluir en el correo electrónico a los toots denunciados send_email_notification: Notificar al usuario por correo electrónico text: Aviso personalizado type: Acción @@ -111,6 +116,7 @@ es: setting_show_application: Mostrar aplicación usada para publicar toots setting_system_font_ui: Utilizar la tipografía por defecto del sistema setting_theme: Tema del sitio + setting_trends: Mostrar las tendencias de hoy setting_unfollow_modal: Mostrar diálogo de confirmación antes de dejar de seguir a alguien setting_use_blurhash: Mostrar gradientes coloridos para contenido multimedia oculto setting_use_pending_items: Modo lento @@ -136,6 +142,12 @@ es: pending_account: Enviar correo electrónico cuando una nueva cuenta necesita revisión reblog: Enviar correo electrónico cuando alguien comparta su publicación report: Enviar un correo cuando se envía un nuevo informe + trending_tag: Enviar correo electrónico cuando una etiqueta no revisada está de tendencia + tag: + listable: Permitir que esta etiqueta aparezca en las búsquedas y en el directorio del perfil + name: Etiqueta + trendable: Permitir que esta etiqueta aparezca bajo tendencias + usable: Permitir a los toots usar esta etiqueta 'no': 'No' recommended: Recomendado required: diff --git a/config/locales/simple_form.nn.yml b/config/locales/simple_form.nn.yml new file mode 100644 index 0000000000..777f4e600f --- /dev/null +++ b/config/locales/simple_form.nn.yml @@ -0,0 +1 @@ +nn: diff --git a/config/locales/simple_form.pt.yml b/config/locales/simple_form.pt-PT.yml similarity index 99% rename from config/locales/simple_form.pt.yml rename to config/locales/simple_form.pt-PT.yml index 9f9d0fdc24..0ac31f8c2b 100644 --- a/config/locales/simple_form.pt.yml +++ b/config/locales/simple_form.pt-PT.yml @@ -1,5 +1,5 @@ --- -pt: +pt-PT: simple_form: hints: account_warning_preset: diff --git a/config/locales/simple_form.ru.yml b/config/locales/simple_form.ru.yml index c4560100aa..ab5eb855e2 100644 --- a/config/locales/simple_form.ru.yml +++ b/config/locales/simple_form.ru.yml @@ -97,11 +97,11 @@ ru: setting_advanced_layout: Включить многоколоночный интерфейс setting_aggregate_reblogs: Группировать продвижения в лентах setting_auto_play_gif: Автоматически проигрывать анимированные GIF - setting_boost_modal: Показывать диалог подтверждения перед продвижением + setting_boost_modal: Всегда спрашивать перед продвижением setting_default_language: Язык отправляемых статусов setting_default_privacy: Видимость постов setting_default_sensitive: Всегда отмечать медиаконтент как чувствительный - setting_delete_modal: Показывать диалог подтверждения перед удалением + setting_delete_modal: Всегда спрашивать перед удалении поста setting_display_media: Отображение медиафайлов setting_display_media_default: По умолчанию setting_display_media_hide_all: Скрывать все @@ -114,7 +114,7 @@ ru: setting_system_font_ui: Использовать шрифт системы по умолчанию setting_theme: Тема сайта setting_trends: Показывать сегодняшние тренды - setting_unfollow_modal: Показывать диалог подтверждения перед тем, как отписаться от аккаунта + setting_unfollow_modal: Всегда спрашивать перед отпиской от аккаунта setting_use_blurhash: Показать цветные градиенты для скрытых медиа setting_use_pending_items: Медленный режим severity: Строгость diff --git a/config/locales/sk.yml b/config/locales/sk.yml index 980e4613ec..e6a30f0c34 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -20,7 +20,7 @@ sk: extended_description_html: |

Pravidlá

Žiadne zatiaľ uvedené nie sú

- federation_hint_html: S účtom na %{instance} budeš môcť následovať ľúdí na hociakom Mastodon serveri, ale aj inde. + federation_hint_html: S účtom na %{instance} budeš môcť následovať ľúdí na hociakom Mastodon serveri, ale aj na iných serveroch. generic_description: "%{domain} je jeden server v sieti" get_apps: Vyskúšaj aplikácie hosted_on: Mastodon hostovaný na %{domain} @@ -28,7 +28,7 @@ sk: Tento účet je virtuálnym aktérom, ktorý predstavuje samotný server a nie žiadného jedného užívateľa. Je využívaný pre potreby federovania a nemal by byť blokovaný, pokiaľ nechceš zablokovať celý server, čo ide lepšie dosiahnúť cez blokovanie domény. learn_more: Zisti viac - privacy_policy: Ustanovenia o súkromí + privacy_policy: Zásady súkromia see_whats_happening: Pozoruj, čo sa deje server_stats: 'Serverové štatistiky:' source_code: Zdrojový kód @@ -233,10 +233,12 @@ sk: deleted_status: "(zmazaný príspevok)" title: Kontrólny záznam custom_emojis: + assign_category: Priraď kategóriu by_domain: Doména copied_msg: Miestna kópia emoji bola úspešne vytvorená copy: Kopíruj copy_failed_msg: Nebolo možné vytvoriť miestnu kópiu tohto emoji + create_new_category: Vytvor novú kategóriu created_msg: Emoji úspešne vytvorené! delete: Zmaž destroyed_msg: Emoji úspešne zničené! @@ -253,6 +255,7 @@ sk: shortcode: Skratka shortcode_hint: Aspoň 2 znaky, povolené sú alfanumerické, alebo podčiarkovník title: Vlastné emoji + uncategorized: Nezaradené unlisted: Nie je na zozname update_failed_msg: Nebolo možné aktualizovať toto emoji updated_msg: Emoji bolo úspešne aktualizované! @@ -580,6 +583,7 @@ sk: description: prefix_invited_by_user: "@%{name} ťa pozýva na tento Mastodon server!" prefix_sign_up: Zaregistruj sa na Mastodone už dnes! + suffix: S pomocou účtu budeš môcť následovať ľudí, posielať príspevky, a vymienať si správy s užívateľmi na hociakom Mastodon serveri, ale aj na iných serveroch! didnt_get_confirmation: Neobdržal/a si kroky na potvrdenie? forgot_password: Zabudnuté heslo? invalid_reset_password_token: Token na obnovu hesla vypršal. Prosím vypítaj si nový. @@ -630,13 +634,15 @@ sk: x_months: "%{count}mesiace" x_seconds: "%{count}sek" deletes: - bad_password_msg: Dobrý pokus, hakeri! Nesprávne heslo confirm_password: Napíšte svoje terajšie heslo pre overenie vašej identity proceed: Vymaž účet success_msg: Tvoj účet bol úspešne vymazaný warning: before: 'Predtým, než budeš pokračovať, prosím pozorne si prečítaj tieto poznámky:' caches: Obsah, ktorý bol predčítaný inými servermi môže zanechať pozostatky + data_removal: Tvoje príspevky a iné dáta budú natrvalo odstránené + more_details_html: Pre viac podrobností, pozri zásady súkromia. + username_available: Tvoje užívateľské meno bude znova dostupné username_unavailable: Tvoja prezývka ostane neprístupná directories: directory: Katalóg profilov @@ -660,10 +666,10 @@ sk: domain_validator: invalid_domain: nieje správny tvar domény errors: - '400': The request you submitted was invalid or malformed. + '400': Požiadavka, ktorú si odoslal/a, bola buď nesprávna, alebo znehodnotená. '403': Nemáš povolenie pre zobrazenie tejto stránky. '404': Stránka ktorú hľadáš nieje tu. - '406': This page is not available in the requested format. + '406': Táto stránka nie je dostupná v požadovanom formáte. '410': Stránka ktorú si tu hľadal/a sa tu už viac nenachádza. '422': content: Bezpečtnostné overenie zlyhalo. Blokuješ cookies? @@ -672,7 +678,7 @@ sk: '500': content: Ospravedlňujem sa. Niečo sa pokazilo na našom konci. title: Táto stránka nieje v poriadku - '503': The page could not be served due to a temporary server failure. + '503': Táto stránka nemôže byť načítaná, kvôli dočasnému výpadku servera. noscript_html: Aby bolo možné používať Mastodon web aplikáciu, povoľ prosím JavaScript. Alebo skús jednu z aplikácii dostupných pre vašu platformu. existing_username_validator: not_found: nepodarilo sa nájsť miestného užívateľa s takouto prezývkou @@ -721,6 +727,7 @@ sk: all: Všetko changes_saved_msg: Zmeny boli úspešne uložené! copy: Kopíruj + no_batch_actions_available: Na tejto stránke niesú k dispozícii žiadne hromadné akcie order_by: Zoraď podľa save_changes: Ulož zmeny validation_errors: diff --git a/config/locales/sl.yml b/config/locales/sl.yml index 02507923be..47b835646b 100644 --- a/config/locales/sl.yml +++ b/config/locales/sl.yml @@ -591,7 +591,6 @@ sl: x_months: "%{count}mo" x_seconds: "%{count}s" deletes: - bad_password_msg: Lep poskus, hekerji! napačno geslo confirm_password: Vnesite svoje trenutno geslo, da potrdite svojo identiteto proceed: Izbriši račun success_msg: Vaš račun je bil uspešno izbrisan diff --git a/config/locales/sq.yml b/config/locales/sq.yml index 68754ea24b..4e5f372947 100644 --- a/config/locales/sq.yml +++ b/config/locales/sq.yml @@ -490,7 +490,6 @@ sq: over_x_years: "%{count}v" x_months: "%{count}mj" deletes: - bad_password_msg: Provë e bukur, trimosha! Fjalëkalim i pasaktë confirm_password: Jepni fjalëkalimin tuaj të tanishëm që të verifikohet identiteti juaj proceed: Fshini llogarinë success_msg: Llogaria juaj u fshi me sukses diff --git a/config/locales/sr-Latn.yml b/config/locales/sr-Latn.yml index c4a3199641..5c06242cc1 100644 --- a/config/locales/sr-Latn.yml +++ b/config/locales/sr-Latn.yml @@ -314,7 +314,6 @@ sr-Latn: over_x_years: "%{count}god" x_months: "%{count}mesec" deletes: - bad_password_msg: Dobar pokušaj, hakeri! Neispravna lozinka confirm_password: Unesite trenutnu lozinku da bismo proverili Vaš identitet proceed: Obriši nalog success_msg: Vaš nalog je uspešno obrisan diff --git a/config/locales/sr.yml b/config/locales/sr.yml index 992311201b..772c04d642 100644 --- a/config/locales/sr.yml +++ b/config/locales/sr.yml @@ -510,7 +510,6 @@ sr: x_days: "%{count}д" x_months: "%{count}месец" deletes: - bad_password_msg: Добар покушај, хакери! Неисправна лозинка confirm_password: Унесите тренутну лозинку да бисмо проверили Ваш идентитет proceed: Обриши налог success_msg: Ваш налог је успешно обрисан diff --git a/config/locales/sv.yml b/config/locales/sv.yml index 0297046719..a71ea9e185 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -377,7 +377,6 @@ sv: x_months: "%{count}mån" x_seconds: "%{count}sek" deletes: - bad_password_msg: Bra försök, hackare! Fel lösenord confirm_password: Ange ditt lösenord för att verifiera din identitet proceed: Ta bort konto success_msg: Ditt konto har tagits bort diff --git a/config/locales/th.yml b/config/locales/th.yml index f27c066175..97ef414608 100644 --- a/config/locales/th.yml +++ b/config/locales/th.yml @@ -48,6 +48,7 @@ th: media: สื่อ moved_html: "%{name} ได้ย้ายไปยัง %{new_profile_link}:" network_hidden: ไม่มีข้อมูลนี้ + never_active: ไม่เลย nothing_here: ไม่มีสิ่งใดที่นี่! people_followed_by: ผู้คนที่ %{name} ติดตาม people_who_follow: ผู้คนที่ติดตาม %{name} diff --git a/config/locales/uk.yml b/config/locales/uk.yml index 564b21db1a..5edbbd1940 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -633,7 +633,6 @@ uk: x_months: "%{count}міс" x_seconds: "%{count}сек" deletes: - bad_password_msg: Гарна спроба, гакери! Неправильний пароль confirm_password: Введіть актуальний пароль, щоб перевірити що ви це ви proceed: Видалити обліковий запис success_msg: Ваш обліковий запис було успішно видалено diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index d2549bcb4d..9c6fd27e8d 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -586,7 +586,6 @@ zh-CN: x_months: "%{count}个月" x_seconds: "%{count}秒" deletes: - bad_password_msg: 想得美,黑客!密码输入错误 confirm_password: 输入你当前的密码来验证身份 proceed: 删除帐户 success_msg: 你的帐户已经成功删除 diff --git a/config/locales/zh-HK.yml b/config/locales/zh-HK.yml index 75202fa688..2c59b3f076 100644 --- a/config/locales/zh-HK.yml +++ b/config/locales/zh-HK.yml @@ -391,7 +391,6 @@ zh-HK: x_months: "%{count}個月" x_seconds: "%{count}秒" deletes: - bad_password_msg: 想得美,黑客!密碼輸入錯誤 confirm_password: 輸入你現在的密碼來驗證身份 proceed: 刪除帳戶 success_msg: 你的帳戶已經成功刪除 diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index 95f7d7f9ac..8bdbf87aa5 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -465,7 +465,6 @@ zh-TW: x_months: "%{count}個月" x_seconds: "%{count}秒" deletes: - bad_password_msg: 想得美,駭客! 密碼輸入錯誤 confirm_password: 輸入你現在的密碼來驗證身份 proceed: 刪除帳戶 success_msg: 你的帳戶已經成功刪除 diff --git a/crowdin.yml b/crowdin.yml index f94417f2ed..88a24d6211 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,4 +1,4 @@ -commit_message: "[ci skip]" +commit_message: '[ci skip]' files: - source: /app/javascript/mastodon/locales/en.json translation: /app/javascript/mastodon/locales/%two_letters_code%.json diff --git a/db/migrate/20190917213523_add_remember_token_index.rb b/db/migrate/20190917213523_add_remember_token_index.rb new file mode 100644 index 0000000000..c5b41ce644 --- /dev/null +++ b/db/migrate/20190917213523_add_remember_token_index.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddRememberTokenIndex < ActiveRecord::Migration[5.2] + disable_ddl_transaction! + + def change + add_index :users, :remember_token, algorithm: :concurrently, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 251aee7395..c4b25d9910 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_09_04_222339) do +ActiveRecord::Schema.define(version: 2019_09_17_213523) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -756,6 +756,7 @@ ActiveRecord::Schema.define(version: 2019_09_04_222339) do t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["created_by_application_id"], name: "index_users_on_created_by_application_id" t.index ["email"], name: "index_users_on_email", unique: true + t.index ["remember_token"], name: "index_users_on_remember_token", unique: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end diff --git a/package.json b/package.json index 3cdd0abcb7..97d3ec497c 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "detect-passive-events": "^1.0.2", "dotenv": "^8.0.0", "emoji-mart": "Gargron/emoji-mart#build", - "es6-symbol": "^3.1.1", + "es6-symbol": "^3.1.2", "escape-html": "^1.0.3", "exif-js": "^2.3.0", "express": "^4.17.1", @@ -101,6 +101,7 @@ "favico.js": "^0.3.10", "font-awesome": "^4.7.0", "glob": "^7.1.1", + "history": "^4.10.1", "http-link-header": "^1.0.2", "immutable": "^3.8.2", "imports-loader": "^0.8.0", @@ -177,7 +178,7 @@ "enzyme": "^3.10.0", "enzyme-adapter-react-16": "^1.14.0", "eslint": "^6.1.0", - "eslint-plugin-import": "~2.18.0", + "eslint-plugin-import": "~2.18.2", "eslint-plugin-jsx-a11y": "~6.2.3", "eslint-plugin-promise": "~4.2.1", "eslint-plugin-react": "~7.14.3", diff --git a/spec/controllers/auth/sessions_controller_spec.rb b/spec/controllers/auth/sessions_controller_spec.rb index 87ef4f2bb2..7ed5edde0c 100644 --- a/spec/controllers/auth/sessions_controller_spec.rb +++ b/spec/controllers/auth/sessions_controller_spec.rb @@ -5,11 +5,11 @@ require 'rails_helper' RSpec.describe Auth::SessionsController, type: :controller do render_views - describe 'GET #new' do - before do - request.env['devise.mapping'] = Devise.mappings[:user] - end + before do + request.env['devise.mapping'] = Devise.mappings[:user] + end + describe 'GET #new' do it 'returns http success' do get :new expect(response).to have_http_status(200) @@ -19,10 +19,6 @@ RSpec.describe Auth::SessionsController, type: :controller do describe 'DELETE #destroy' do let(:user) { Fabricate(:user) } - before do - request.env['devise.mapping'] = Devise.mappings[:user] - end - context 'with a regular user' do it 'redirects to home after sign out' do sign_in(user, scope: :user) @@ -51,10 +47,6 @@ RSpec.describe Auth::SessionsController, type: :controller do end describe 'POST #create' do - before do - request.env['devise.mapping'] = Devise.mappings[:user] - end - context 'using PAM authentication', if: ENV['PAM_ENABLED'] == 'true' do context 'using a valid password' do before do @@ -191,11 +183,11 @@ RSpec.describe Auth::SessionsController, type: :controller do end context 'using two-factor authentication' do - let(:user) do - Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', - otp_required_for_login: true, otp_secret: User.generate_otp_secret(32)) + let!(:user) do + Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32)) end - let(:recovery_codes) do + + let!(:recovery_codes) do codes = user.generate_otp_backup_codes! user.save return codes diff --git a/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb b/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb index 2222a7559b..2e5a9325cf 100644 --- a/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb +++ b/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb @@ -68,7 +68,7 @@ describe Settings::TwoFactorAuthentication::ConfirmationsController do true end - post :create, params: { form_two_factor_confirmation: { code: '123456' } } + post :create, params: { form_two_factor_confirmation: { otp_attempt: '123456' } } expect(assigns(:recovery_codes)).to eq otp_backup_codes expect(flash[:notice]).to eq 'Two-factor authentication successfully enabled' @@ -85,7 +85,7 @@ describe Settings::TwoFactorAuthentication::ConfirmationsController do false end - post :create, params: { form_two_factor_confirmation: { code: '123456' } } + post :create, params: { form_two_factor_confirmation: { otp_attempt: '123456' } } end it 'renders the new view' do @@ -99,7 +99,7 @@ describe Settings::TwoFactorAuthentication::ConfirmationsController do context 'when not signed in' do it 'redirects if not signed in' do - post :create, params: { form_two_factor_confirmation: { code: '123456' } } + post :create, params: { form_two_factor_confirmation: { otp_attempt: '123456' } } expect(response).to redirect_to('/auth/sign_in') end end diff --git a/spec/controllers/settings/two_factor_authentications_controller_spec.rb b/spec/controllers/settings/two_factor_authentications_controller_spec.rb index f7c6287569..922231ded6 100644 --- a/spec/controllers/settings/two_factor_authentications_controller_spec.rb +++ b/spec/controllers/settings/two_factor_authentications_controller_spec.rb @@ -91,7 +91,7 @@ describe Settings::TwoFactorAuthenticationsController do true end - post :destroy, params: { form_two_factor_confirmation: { code: '123456' } } + post :destroy, params: { form_two_factor_confirmation: { otp_attempt: '123456' } } expect(response).to redirect_to(settings_two_factor_authentication_path) user.reload @@ -105,7 +105,7 @@ describe Settings::TwoFactorAuthenticationsController do false end - post :destroy, params: { form_two_factor_confirmation: { code: '057772' } } + post :destroy, params: { form_two_factor_confirmation: { otp_attempt: '057772' } } user.reload expect(user.otp_required_for_login).to eq(true) diff --git a/spec/lib/spam_check_spec.rb b/spec/lib/spam_check_spec.rb index 9e0989216a..4cae461117 100644 --- a/spec/lib/spam_check_spec.rb +++ b/spec/lib/spam_check_spec.rb @@ -86,23 +86,33 @@ RSpec.describe SpamCheck do end it 'returns true for duplicate statuses to the same recipient' do - status1 = status_with_html('@alice Hello') - described_class.new(status1).remember! + described_class::THRESHOLD.times do + status1 = status_with_html('@alice Hello') + described_class.new(status1).remember! + end + status2 = status_with_html('@alice Hello') expect(described_class.new(status2).spam?).to be true end it 'returns true for duplicate statuses to different recipients' do - status1 = status_with_html('@alice Hello') - described_class.new(status1).remember! + described_class::THRESHOLD.times do + status1 = status_with_html('@alice Hello') + described_class.new(status1).remember! + end + status2 = status_with_html('@bob Hello') expect(described_class.new(status2).spam?).to be true end it 'returns true for nearly identical statuses with random numbers' do source_text = 'Sodium, atomic number 11, was first isolated by Humphry Davy in 1807. A chemical component of salt, he named it Na in honor of the saltiest region on earth, North America.' - status1 = status_with_html('@alice ' + source_text + ' 1234') - described_class.new(status1).remember! + + described_class::THRESHOLD.times do + status1 = status_with_html('@alice ' + source_text + ' 1234') + described_class.new(status1).remember! + end + status2 = status_with_html('@bob ' + source_text + ' 9568') expect(described_class.new(status2).spam?).to be true end @@ -140,9 +150,9 @@ RSpec.describe SpamCheck do let(:redis_key) { spam_check.send(:redis_key) } it 'remembers' do - expect do - spam_check.remember! - end.to change { Redis.current.exists(redis_key) }.from(false).to(true) + expect(Redis.current.exists(redis_key)).to be true + spam_check.remember! + expect(Redis.current.exists(redis_key)).to be true end end @@ -156,9 +166,9 @@ RSpec.describe SpamCheck do end it 'resets' do - expect do - spam_check.reset! - end.to change { Redis.current.exists(redis_key) }.from(true).to(false) + expect(Redis.current.exists(redis_key)).to be true + spam_check.reset! + expect(Redis.current.exists(redis_key)).to be false end end diff --git a/yarn.lock b/yarn.lock index 8bcdc0818f..7811058c07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3185,12 +3185,13 @@ cyclist@~0.2.2: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA= -d@1: - version "1.0.0" - resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" - integrity sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8= +d@1, d@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" + integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA== dependencies: - es5-ext "^0.10.9" + es5-ext "^0.10.50" + type "^1.0.1" damerau-levenshtein@^1.0.4: version "1.0.4" @@ -3729,24 +3730,15 @@ es-to-primitive@^1.1.1, es-to-primitive@^1.2.0: is-date-object "^1.0.1" is-symbol "^1.0.2" -es5-ext@^0.10.14: - version "0.10.50" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.50.tgz#6d0e23a0abdb27018e5ac4fd09b412bc5517a778" - integrity sha512-KMzZTPBkeQV/JcSQhI5/z6d9VWJ3EnQ194USTUwIYZ2ZbpN8+SGXQKt1h68EX44+qt+Fzr8DO17vnxrw7c3agw== +es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.50, es5-ext@^0.10.51, es5-ext@~0.10.14: + version "0.10.51" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.51.tgz#ed2d7d9d48a12df86e0299287e93a09ff478842f" + integrity sha512-oRpWzM2WcLHVKpnrcyB7OW8j/s67Ba04JCm0WnNv3RiABSvs7mrQlutB8DBv793gKcp0XENR8Il8WxGTlZ73gQ== dependencies: es6-iterator "~2.0.3" es6-symbol "~3.1.1" next-tick "^1.0.0" -es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14: - version "0.10.46" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.46.tgz#efd99f67c5a7ec789baa3daa7f79870388f7f572" - integrity sha512-24XxRvJXNFwEMpJb3nOkiRJKRoupmjYmOPVlI65Qy2SrtxwOTB+g6ODjBKOtwEHbYrhWRty9xxOWLNdClT2djw== - dependencies: - es6-iterator "~2.0.3" - es6-symbol "~3.1.1" - next-tick "1" - es6-iterator@^2.0.1, es6-iterator@~2.0.1, es6-iterator@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" @@ -3779,7 +3771,7 @@ es6-set@~0.1.5: es6-symbol "3.1.1" event-emitter "~0.3.5" -es6-symbol@3.1.1, es6-symbol@^3.1.1, es6-symbol@~3.1.1: +es6-symbol@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77" integrity sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc= @@ -3787,6 +3779,14 @@ es6-symbol@3.1.1, es6-symbol@^3.1.1, es6-symbol@~3.1.1: d "1" es5-ext "~0.10.14" +es6-symbol@^3.1.1, es6-symbol@^3.1.2, es6-symbol@~3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.2.tgz#859fdd34f32e905ff06d752e7171ddd4444a7ed1" + integrity sha512-/ZypxQsArlv+KHpGvng52/Iz8by3EQPxhmbuz8yFG89N/caTFBSbcXONDw0aMjy827gQg26XAjP4uXFvnfINmQ== + dependencies: + d "^1.0.1" + es5-ext "^0.10.51" + es6-weak-map@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.2.tgz#5e3ab32251ffd1538a1f8e5ffa1357772f92d96f" @@ -3845,10 +3845,10 @@ eslint-module-utils@^2.4.0: debug "^2.6.8" pkg-dir "^2.0.0" -eslint-plugin-import@~2.18.0: - version "2.18.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.18.0.tgz#7a5ba8d32622fb35eb9c8db195c2090bd18a3678" - integrity sha512-PZpAEC4gj/6DEMMoU2Df01C5c50r7zdGIN52Yfi7CvvWaYssG7Jt5R9nFG5gmqodxNOz9vQS87xk6Izdtpdrig== +eslint-plugin-import@~2.18.2: + version "2.18.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.18.2.tgz#02f1180b90b077b33d447a17a2326ceb400aceb6" + integrity sha512-5ohpsHAiUBRNaBWAF08izwUGlbrJoJJ+W9/TBwsGoR1MnlgfwMIKrFeSjWbt6moabiXW9xNvtFz+97KHRfI4HQ== dependencies: array-includes "^3.0.3" contains-path "^0.1.0" @@ -3857,8 +3857,8 @@ eslint-plugin-import@~2.18.0: eslint-import-resolver-node "^0.3.2" eslint-module-utils "^2.4.0" has "^1.0.3" - lodash "^4.17.11" minimatch "^3.0.4" + object.values "^1.1.0" read-pkg-up "^2.0.0" resolve "^1.11.0" @@ -4934,6 +4934,18 @@ hex-color-regex@^1.1.0: resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== +history@^4.10.1: + version "4.10.1" + resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" + integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== + dependencies: + "@babel/runtime" "^7.1.2" + loose-envify "^1.2.0" + resolve-pathname "^3.0.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + value-equal "^1.0.1" + history@^4.7.2: version "4.7.2" resolved "https://registry.yarnpkg.com/history/-/history-4.7.2.tgz#22b5c7f31633c5b8021c7f4a8a954ac139ee8d5b" @@ -7039,7 +7051,7 @@ neo-async@^2.5.0, neo-async@^2.6.0: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== -next-tick@1, next-tick@^1.0.0: +next-tick@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= @@ -9174,6 +9186,11 @@ resolve-pathname@^2.2.0: resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-2.2.0.tgz#7e9ae21ed815fd63ab189adeee64dc831eefa879" integrity sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg== +resolve-pathname@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" + integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== + resolve-url@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" @@ -10209,11 +10226,21 @@ timsort@^0.3.0: resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= +tiny-invariant@^1.0.2: + version "1.0.6" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.6.tgz#b3f9b38835e36a41c843a3b0907a5a7b3755de73" + integrity sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA== + tiny-queue@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/tiny-queue/-/tiny-queue-0.2.1.tgz#25a67f2c6e253b2ca941977b5ef7442ef97a6046" integrity sha1-JaZ/LG4lOyypQZd7XvdELvl6YEY= +tiny-warning@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" + integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== + tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -10350,6 +10377,11 @@ type-is@~1.6.17, type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +type@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/type/-/type-1.0.3.tgz#16f5d39f27a2d28d86e48f8981859e9d3296c179" + integrity sha512-51IMtNfVcee8+9GJvj0spSuFcZHe9vSib6Xtgsny1Km9ugyz2mbS08I3rsUIRYgJohFRFU1160sgRodYz378Hg== + typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" @@ -10553,6 +10585,11 @@ value-equal@^0.4.0: resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-0.4.0.tgz#c5bdd2f54ee093c04839d71ce2e4758a6890abc7" integrity sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw== +value-equal@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" + integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== + vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"