From b8514561394767a10d3cf40132ada24d938c1680 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 7 Jul 2019 16:16:51 +0200 Subject: Remove Atom feeds and old URLs in the form of `GET /:username/updates/:id` (#11247) --- app/helpers/admin/action_logs_helper.rb | 2 +- app/helpers/home_helper.rb | 2 +- app/helpers/statuses_helper.rb | 222 ++++++++++++++++++++++++++++++++ app/helpers/stream_entries_helper.rb | 220 ------------------------------- 4 files changed, 224 insertions(+), 222 deletions(-) create mode 100644 app/helpers/statuses_helper.rb delete mode 100644 app/helpers/stream_entries_helper.rb (limited to 'app/helpers') diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb index e5fbb1500..1daa60774 100644 --- a/app/helpers/admin/action_logs_helper.rb +++ b/app/helpers/admin/action_logs_helper.rb @@ -89,7 +89,7 @@ module Admin::ActionLogsHelper when 'DomainBlock', 'EmailDomainBlock' link_to record.domain, "https://#{record.domain}" when 'Status' - link_to record.account.acct, TagManager.instance.url_for(record) + link_to record.account.acct, ActivityPub::TagManager.instance.url_for(record) when 'AccountWarning' link_to record.target_account.acct, admin_account_path(record.target_account_id) end diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb index df60b7dd7..b66e827fe 100644 --- a/app/helpers/home_helper.rb +++ b/app/helpers/home_helper.rb @@ -21,7 +21,7 @@ module HomeHelper end end else - link_to(path || TagManager.instance.url_for(account), class: 'account__display-name') do + link_to(path || ActivityPub::TagManager.instance.url_for(account), class: 'account__display-name') do content_tag(:div, class: 'account__avatar-wrapper') do content_tag(:div, '', class: 'account__avatar', style: "width: #{size}px; height: #{size}px; background-size: #{size}px #{size}px; background-image: url(#{full_asset_url(current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url)})") end + diff --git a/app/helpers/statuses_helper.rb b/app/helpers/statuses_helper.rb new file mode 100644 index 000000000..e067380f6 --- /dev/null +++ b/app/helpers/statuses_helper.rb @@ -0,0 +1,222 @@ +# frozen_string_literal: true + +module StatusesHelper + EMBEDDED_CONTROLLER = 'statuses' + EMBEDDED_ACTION = 'embed' + + def display_name(account, **options) + if options[:custom_emojify] + Formatter.instance.format_display_name(account, options) + else + account.display_name.presence || account.username + end + end + + def account_action_button(account) + if user_signed_in? + if account.id == current_user.account_id + link_to settings_profile_url, class: 'button logo-button' do + safe_join([svg_logo, t('settings.edit_profile')]) + end + elsif current_account.following?(account) || current_account.requested?(account) + link_to account_unfollow_path(account), class: 'button logo-button button--destructive', data: { method: :post } do + safe_join([svg_logo, t('accounts.unfollow')]) + end + elsif !(account.memorial? || account.moved?) + link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do + safe_join([svg_logo, t('accounts.follow')]) + end + end + elsif !(account.memorial? || account.moved?) + link_to account_remote_follow_path(account), class: 'button logo-button modal-button', target: '_new' do + safe_join([svg_logo, t('accounts.follow')]) + end + end + end + + def svg_logo + content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976') + end + + def svg_logo_full + content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo-full'), 'viewBox' => '0 0 713.35878 175.8678') + end + + def account_badge(account, all: false) + if account.bot? + content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles') + elsif (Setting.show_staff_badge && account.user_staff?) || all + content_tag(:div, class: 'roles') do + if all && !account.user_staff? + content_tag(:div, t('admin.accounts.roles.user'), class: 'account-role') + elsif account.user_admin? + content_tag(:div, t('accounts.roles.admin'), class: 'account-role admin') + elsif account.user_moderator? + content_tag(:div, t('accounts.roles.moderator'), class: 'account-role moderator') + end + end + end + end + + def link_to_more(url) + link_to t('statuses.show_more'), url, class: 'load-more load-gap' + end + + def nothing_here(extra_classes = '') + content_tag(:div, class: "nothing-here #{extra_classes}") do + t('accounts.nothing_here') + end + end + + def account_description(account) + prepend_str = [ + [ + number_to_human(account.statuses_count, strip_insignificant_zeros: true), + I18n.t('accounts.posts', count: account.statuses_count), + ].join(' '), + + [ + number_to_human(account.following_count, strip_insignificant_zeros: true), + I18n.t('accounts.following', count: account.following_count), + ].join(' '), + + [ + number_to_human(account.followers_count, strip_insignificant_zeros: true), + I18n.t('accounts.followers', count: account.followers_count), + ].join(' '), + ].join(', ') + + [prepend_str, account.note].join(' · ') + end + + def media_summary(status) + attachments = { image: 0, video: 0 } + + status.media_attachments.each do |media| + if media.video? + attachments[:video] += 1 + else + attachments[:image] += 1 + end + end + + text = attachments.to_a.reject { |_, value| value.zero? }.map { |key, value| I18n.t("statuses.attached.#{key}", count: value) }.join(' · ') + + return if text.blank? + + I18n.t('statuses.attached.description', attached: text) + end + + def status_text_summary(status) + return if status.spoiler_text.blank? + + I18n.t('statuses.content_warning', warning: status.spoiler_text) + end + + def poll_summary(status) + return unless status.preloadable_poll + + status.preloadable_poll.options.map { |o| "[ ] #{o}" }.join("\n") + end + + def status_description(status) + components = [[media_summary(status), status_text_summary(status)].reject(&:blank?).join(' · ')] + + if status.spoiler_text.blank? + components << status.text + components << poll_summary(status) + end + + components.reject(&:blank?).join("\n\n") + end + + def stream_link_target + embedded_view? ? '_blank' : nil + end + + def acct(account) + if account.local? + "@#{account.acct}@#{Rails.configuration.x.local_domain}" + else + "@#{account.acct}" + end + end + + def style_classes(status, is_predecessor, is_successor, include_threads) + classes = ['entry'] + classes << 'entry-predecessor' if is_predecessor + classes << 'entry-reblog' if status.reblog? + classes << 'entry-successor' if is_successor + classes << 'entry-center' if include_threads + classes.join(' ') + end + + def microformats_classes(status, is_direct_parent, is_direct_child) + classes = [] + classes << 'p-in-reply-to' if is_direct_parent + classes << 'p-repost-of' if status.reblog? && is_direct_parent + classes << 'p-comment' if is_direct_child + classes.join(' ') + end + + def microformats_h_class(status, is_predecessor, is_successor, include_threads) + if is_predecessor || status.reblog? || is_successor + 'h-cite' + elsif include_threads + '' + else + 'h-entry' + end + end + + def rtl_status?(status) + status.local? ? rtl?(status.text) : rtl?(strip_tags(status.text)) + end + + def rtl?(text) + text = simplified_text(text) + rtl_words = text.scan(/[\p{Hebrew}\p{Arabic}\p{Syriac}\p{Thaana}\p{Nko}]+/m) + + if rtl_words.present? + total_size = text.size.to_f + rtl_size(rtl_words) / total_size > 0.3 + else + false + end + end + + def fa_visibility_icon(status) + case status.visibility + when 'public' + fa_icon 'globe fw' + when 'unlisted' + fa_icon 'unlock fw' + when 'private' + fa_icon 'lock fw' + when 'direct' + fa_icon 'envelope fw' + end + end + + private + + def simplified_text(text) + text.dup.tap do |new_text| + URI.extract(new_text).each do |url| + new_text.gsub!(url, '') + end + + new_text.gsub!(Account::MENTION_RE, '') + new_text.gsub!(Tag::HASHTAG_RE, '') + new_text.gsub!(/\s+/, '') + end + end + + def rtl_size(words) + words.reduce(0) { |acc, elem| acc + elem.size }.to_f + end + + def embedded_view? + params[:controller] == EMBEDDED_CONTROLLER && params[:action] == EMBEDDED_ACTION + end +end diff --git a/app/helpers/stream_entries_helper.rb b/app/helpers/stream_entries_helper.rb deleted file mode 100644 index 02a860a74..000000000 --- a/app/helpers/stream_entries_helper.rb +++ /dev/null @@ -1,220 +0,0 @@ -# frozen_string_literal: true - -module StreamEntriesHelper - EMBEDDED_CONTROLLER = 'statuses' - EMBEDDED_ACTION = 'embed' - - def display_name(account, **options) - if options[:custom_emojify] - Formatter.instance.format_display_name(account, options) - else - account.display_name.presence || account.username - end - end - - def account_action_button(account) - if user_signed_in? - if account.id == current_user.account_id - link_to settings_profile_url, class: 'button logo-button' do - safe_join([svg_logo, t('settings.edit_profile')]) - end - elsif current_account.following?(account) || current_account.requested?(account) - link_to account_unfollow_path(account), class: 'button logo-button button--destructive', data: { method: :post } do - safe_join([svg_logo, t('accounts.unfollow')]) - end - elsif !(account.memorial? || account.moved?) - link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do - safe_join([svg_logo, t('accounts.follow')]) - end - end - elsif !(account.memorial? || account.moved?) - link_to account_remote_follow_path(account), class: 'button logo-button modal-button', target: '_new' do - safe_join([svg_logo, t('accounts.follow')]) - end - end - end - - def svg_logo - content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976') - end - - def svg_logo_full - content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo-full'), 'viewBox' => '0 0 713.35878 175.8678') - end - - def account_badge(account, all: false) - if account.bot? - content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles') - elsif (Setting.show_staff_badge && account.user_staff?) || all - content_tag(:div, class: 'roles') do - if all && !account.user_staff? - content_tag(:div, t('admin.accounts.roles.user'), class: 'account-role') - elsif account.user_admin? - content_tag(:div, t('accounts.roles.admin'), class: 'account-role admin') - elsif account.user_moderator? - content_tag(:div, t('accounts.roles.moderator'), class: 'account-role moderator') - end - end - end - end - - def link_to_more(url) - link_to t('statuses.show_more'), url, class: 'load-more load-gap' - end - - def nothing_here(extra_classes = '') - content_tag(:div, class: "nothing-here #{extra_classes}") do - t('accounts.nothing_here') - end - end - - def account_description(account) - prepend_str = [ - [ - number_to_human(account.statuses_count, strip_insignificant_zeros: true), - I18n.t('accounts.posts', count: account.statuses_count), - ].join(' '), - - [ - number_to_human(account.following_count, strip_insignificant_zeros: true), - I18n.t('accounts.following', count: account.following_count), - ].join(' '), - - [ - number_to_human(account.followers_count, strip_insignificant_zeros: true), - I18n.t('accounts.followers', count: account.followers_count), - ].join(' '), - ].join(', ') - - [prepend_str, account.note].join(' · ') - end - - def media_summary(status) - attachments = { image: 0, video: 0 } - - status.media_attachments.each do |media| - if media.video? - attachments[:video] += 1 - else - attachments[:image] += 1 - end - end - - text = attachments.to_a.reject { |_, value| value.zero? }.map { |key, value| I18n.t("statuses.attached.#{key}", count: value) }.join(' · ') - - return if text.blank? - - I18n.t('statuses.attached.description', attached: text) - end - - def status_text_summary(status) - return if status.spoiler_text.blank? - I18n.t('statuses.content_warning', warning: status.spoiler_text) - end - - def poll_summary(status) - return unless status.preloadable_poll - status.preloadable_poll.options.map { |o| "[ ] #{o}" }.join("\n") - end - - def status_description(status) - components = [[media_summary(status), status_text_summary(status)].reject(&:blank?).join(' · ')] - - if status.spoiler_text.blank? - components << status.text - components << poll_summary(status) - end - - components.reject(&:blank?).join("\n\n") - end - - def stream_link_target - embedded_view? ? '_blank' : nil - end - - def acct(account) - if account.local? - "@#{account.acct}@#{Rails.configuration.x.local_domain}" - else - "@#{account.acct}" - end - end - - def style_classes(status, is_predecessor, is_successor, include_threads) - classes = ['entry'] - classes << 'entry-predecessor' if is_predecessor - classes << 'entry-reblog' if status.reblog? - classes << 'entry-successor' if is_successor - classes << 'entry-center' if include_threads - classes.join(' ') - end - - def microformats_classes(status, is_direct_parent, is_direct_child) - classes = [] - classes << 'p-in-reply-to' if is_direct_parent - classes << 'p-repost-of' if status.reblog? && is_direct_parent - classes << 'p-comment' if is_direct_child - classes.join(' ') - end - - def microformats_h_class(status, is_predecessor, is_successor, include_threads) - if is_predecessor || status.reblog? || is_successor - 'h-cite' - elsif include_threads - '' - else - 'h-entry' - end - end - - def rtl_status?(status) - status.local? ? rtl?(status.text) : rtl?(strip_tags(status.text)) - end - - def rtl?(text) - text = simplified_text(text) - rtl_words = text.scan(/[\p{Hebrew}\p{Arabic}\p{Syriac}\p{Thaana}\p{Nko}]+/m) - - if rtl_words.present? - total_size = text.size.to_f - rtl_size(rtl_words) / total_size > 0.3 - else - false - end - end - - def fa_visibility_icon(status) - case status.visibility - when 'public' - fa_icon 'globe fw' - when 'unlisted' - fa_icon 'unlock fw' - when 'private' - fa_icon 'lock fw' - when 'direct' - fa_icon 'envelope fw' - end - end - - private - - def simplified_text(text) - text.dup.tap do |new_text| - URI.extract(new_text).each do |url| - new_text.gsub!(url, '') - end - - new_text.gsub!(Account::MENTION_RE, '') - new_text.gsub!(Tag::HASHTAG_RE, '') - new_text.gsub!(/\s+/, '') - end - end - - def rtl_size(words) - words.reduce(0) { |acc, elem| acc + elem.size }.to_f - end - - def embedded_view? - params[:controller] == EMBEDDED_CONTROLLER && params[:action] == EMBEDDED_ACTION - end -end -- cgit From 4e921832272425352d28cad550bfc4dffd6d0e78 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 9 Jul 2019 03:27:35 +0200 Subject: Refactor domain block checks (#11268) --- app/controllers/concerns/signature_verification.rb | 4 + app/helpers/domain_control_helper.rb | 17 ++++ app/lib/tag_manager.rb | 3 + .../fetch_featured_collection_service.rb | 3 +- .../activitypub/fetch_remote_account_service.rb | 14 ++- .../activitypub/fetch_remote_poll_service.rb | 2 + .../activitypub/process_account_service.rb | 5 +- .../activitypub/process_collection_service.rb | 4 +- app/services/activitypub/process_poll_service.rb | 1 + app/services/resolve_account_service.rb | 101 +++++++++++++-------- spec/services/resolve_account_service_spec.rb | 5 + 11 files changed, 108 insertions(+), 51 deletions(-) create mode 100644 app/helpers/domain_control_helper.rb (limited to 'app/helpers') diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index 90a57197c..0ccdf5ec9 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -5,6 +5,8 @@ module SignatureVerification extend ActiveSupport::Concern + include DomainControlHelper + def signed_request? request.headers['Signature'].present? end @@ -126,6 +128,8 @@ module SignatureVerification if key_id.start_with?('acct:') stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, '')) } elsif !ActivityPub::TagManager.instance.local_uri?(key_id) + return if domain_not_allowed?(key_id) + account = ActivityPub::TagManager.instance.uri_to_resource(key_id, Account) account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false) } account diff --git a/app/helpers/domain_control_helper.rb b/app/helpers/domain_control_helper.rb new file mode 100644 index 000000000..efd328f81 --- /dev/null +++ b/app/helpers/domain_control_helper.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module DomainControlHelper + def domain_not_allowed?(uri_or_domain) + return if uri_or_domain.blank? + + domain = begin + if uri_or_domain.include?('://') + Addressable::URI.parse(uri_or_domain).domain + else + uri_or_domain + end + end + + DomainBlock.blocked?(domain) + end +end diff --git a/app/lib/tag_manager.rb b/app/lib/tag_manager.rb index daf4f556b..c88cf4994 100644 --- a/app/lib/tag_manager.rb +++ b/app/lib/tag_manager.rb @@ -24,13 +24,16 @@ class TagManager def same_acct?(canonical, needle) return true if canonical.casecmp(needle).zero? + username, domain = needle.split('@') + local_domain?(domain) && canonical.casecmp(username).zero? end def local_url?(url) uri = Addressable::URI.parse(url).normalize domain = uri.host + (uri.port ? ":#{uri.port}" : '') + TagManager.instance.web_domain?(domain) end end diff --git a/app/services/activitypub/fetch_featured_collection_service.rb b/app/services/activitypub/fetch_featured_collection_service.rb index 6a137b520..2c2770466 100644 --- a/app/services/activitypub/fetch_featured_collection_service.rb +++ b/app/services/activitypub/fetch_featured_collection_service.rb @@ -4,13 +4,12 @@ class ActivityPub::FetchFeaturedCollectionService < BaseService include JsonLdHelper def call(account) - return if account.featured_collection_url.blank? + return if account.featured_collection_url.blank? || account.suspended? || account.local? @account = account @json = fetch_resource(@account.featured_collection_url, true) return unless supported_context? - return if @account.suspended? || @account.local? case @json['type'] when 'Collection', 'CollectionPage' diff --git a/app/services/activitypub/fetch_remote_account_service.rb b/app/services/activitypub/fetch_remote_account_service.rb index 3c2044941..d65c8f951 100644 --- a/app/services/activitypub/fetch_remote_account_service.rb +++ b/app/services/activitypub/fetch_remote_account_service.rb @@ -2,18 +2,22 @@ class ActivityPub::FetchRemoteAccountService < BaseService include JsonLdHelper + include DomainControlHelper SUPPORTED_TYPES = %w(Application Group Organization Person Service).freeze # Does a WebFinger roundtrip on each call, unless `only_key` is true def call(uri, id: true, prefetched_body: nil, break_on_redirect: false, only_key: false) + return if domain_not_allowed?(uri) return ActivityPub::TagManager.instance.uri_to_resource(uri, Account) if ActivityPub::TagManager.instance.local_uri?(uri) - @json = if prefetched_body.nil? - fetch_resource(uri, id) - else - body_to_json(prefetched_body, compare_id: id ? uri : nil) - end + @json = begin + if prefetched_body.nil? + fetch_resource(uri, id) + else + body_to_json(prefetched_body, compare_id: id ? uri : nil) + end + end return if !supported_context? || !expected_type? || (break_on_redirect && @json['movedTo'].present?) diff --git a/app/services/activitypub/fetch_remote_poll_service.rb b/app/services/activitypub/fetch_remote_poll_service.rb index 854a32d05..1c79ecf11 100644 --- a/app/services/activitypub/fetch_remote_poll_service.rb +++ b/app/services/activitypub/fetch_remote_poll_service.rb @@ -5,7 +5,9 @@ class ActivityPub::FetchRemotePollService < BaseService def call(poll, on_behalf_of = nil) json = fetch_resource(poll.status.uri, true, on_behalf_of) + return unless supported_context?(json) + ActivityPub::ProcessPollService.new.call(poll, json) end end diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index 3857e7c16..603e27ed9 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -2,11 +2,12 @@ class ActivityPub::ProcessAccountService < BaseService include JsonLdHelper + include DomainControlHelper # Should be called with confirmed valid JSON # and WebFinger-resolved username and domain def call(username, domain, json, options = {}) - return if json['inbox'].blank? || unsupported_uri_scheme?(json['id']) + return if json['inbox'].blank? || unsupported_uri_scheme?(json['id']) || domain_not_allowed?(domain) @options = options @json = json @@ -15,8 +16,6 @@ class ActivityPub::ProcessAccountService < BaseService @domain = domain @collections = {} - return if auto_suspend? - RedisLock.acquire(lock_options) do |lock| if lock.acquired? @account = Account.find_remote(@username, @domain) diff --git a/app/services/activitypub/process_collection_service.rb b/app/services/activitypub/process_collection_service.rb index 881df478b..a2a2e7071 100644 --- a/app/services/activitypub/process_collection_service.rb +++ b/app/services/activitypub/process_collection_service.rb @@ -8,9 +8,7 @@ class ActivityPub::ProcessCollectionService < BaseService @json = Oj.load(body, mode: :strict) @options = options - return unless supported_context? - return if different_actor? && verify_account!.nil? - return if @account.suspended? || @account.local? + return if !supported_context? || (different_actor? && verify_account!.nil?) || @account.suspended? || @account.local? case @json['type'] when 'Collection', 'CollectionPage' diff --git a/app/services/activitypub/process_poll_service.rb b/app/services/activitypub/process_poll_service.rb index 61357abd3..2fbce65b9 100644 --- a/app/services/activitypub/process_poll_service.rb +++ b/app/services/activitypub/process_poll_service.rb @@ -5,6 +5,7 @@ class ActivityPub::ProcessPollService < BaseService def call(poll, json) @json = json + return unless expected_type? previous_expires_at = poll.expires_at diff --git a/app/services/resolve_account_service.rb b/app/services/resolve_account_service.rb index 0ea31a0d8..41a2eb158 100644 --- a/app/services/resolve_account_service.rb +++ b/app/services/resolve_account_service.rb @@ -1,75 +1,108 @@ # frozen_string_literal: true -require_relative '../models/account' - class ResolveAccountService < BaseService include JsonLdHelper + include DomainControlHelper + + class WebfingerRedirectError < StandardError; end - # Find or create a local account for a remote user. - # When creating, look up the user's webfinger and fetch all - # important information from their feed - # @param [String, Account] uri User URI in the form of username@domain + # Find or create an account record for a remote user. When creating, + # look up the user's webfinger and fetch ActivityPub data + # @param [String, Account] uri URI in the username@domain format or account record # @param [Hash] options + # @option options [Boolean] :redirected Do not follow further Webfinger redirects + # @option options [Boolean] :skip_webfinger Do not attempt to refresh account data # @return [Account] def call(uri, options = {}) + return if uri.blank? + + process_options!(uri, options) + + # First of all we want to check if we've got the account + # record with the URI already, and if so, we can exit early + + return if domain_not_allowed?(@domain) + + @account ||= Account.find_remote(@username, @domain) + + return @account if @account&.local? || !webfinger_update_due? + + # At this point we are in need of a Webfinger query, which may + # yield us a different username/domain through a redirect + + process_webfinger! + + # Because the username/domain pair may be different than what + # we already checked, we need to check if we've already got + # the record with that URI, again + + return if domain_not_allowed?(@domain) + + @account ||= Account.find_remote(@username, @domain) + + return @account if @account&.local? || !webfinger_update_due? + + # Now it is certain, it is definitely a remote account, and it + # either needs to be created, or updated from fresh data + + process_account! + rescue Goldfinger::Error, WebfingerRedirectError, Oj::ParseError => e + Rails.logger.debug "Webfinger query for #{@uri} failed: #{e}" + nil + end + + private + + def process_options!(uri, options) @options = options if uri.is_a?(Account) @account = uri @username = @account.username @domain = @account.domain - uri = "#{@username}@#{@domain}" - - return @account if @account.local? || !webfinger_update_due? + @uri = [@username, @domain].compact.join('@') else + @uri = uri @username, @domain = uri.split('@') - - return Account.find_local(@username) if TagManager.instance.local_domain?(@domain) - - @account = Account.find_remote(@username, @domain) - - return @account unless webfinger_update_due? end - Rails.logger.debug "Looking up webfinger for #{uri}" - - @webfinger = Goldfinger.finger("acct:#{uri}") + @domain = nil if TagManager.instance.local_domain?(@domain) + end + def process_webfinger! + @webfinger = Goldfinger.finger("acct:#{@uri}") confirmed_username, confirmed_domain = @webfinger.subject.gsub(/\Aacct:/, '').split('@') if confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero? @username = confirmed_username @domain = confirmed_domain - elsif options[:redirected].nil? - return call("#{confirmed_username}@#{confirmed_domain}", options.merge(redirected: true)) + elsif @options[:redirected].nil? + @account = ResolveAccountService.new.call("#{confirmed_username}@#{confirmed_domain}", @options.merge(redirected: true)) else - Rails.logger.debug 'Requested and returned acct URIs do not match' - return + raise WebfingerRedirectError, "The URI #{uri} tries to hijack #{@username}@#{@domain}" end - return Account.find_local(@username) if TagManager.instance.local_domain?(@domain) + @domain = nil if TagManager.instance.local_domain?(@domain) + end + + def process_account! return unless activitypub_ready? RedisLock.acquire(lock_options) do |lock| if lock.acquired? @account = Account.find_remote(@username, @domain) - next unless @account.nil? || @account.activitypub? + next if (@account.present? && !@account.activitypub?) || actor_json.nil? - handle_activitypub + @account = ActivityPub::ProcessAccountService.new.call(@username, @domain, actor_json) else raise Mastodon::RaceConditionError end end @account - rescue Goldfinger::Error => e - Rails.logger.debug "Webfinger query for #{uri} unsuccessful: #{e}" - nil end - private - def webfinger_update_due? @account.nil? || ((!@options[:skip_webfinger] || @account.ostatus?) && @account.possibly_stale?) end @@ -78,14 +111,6 @@ class ResolveAccountService < BaseService !@webfinger.link('self').nil? && ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(@webfinger.link('self').type) end - def handle_activitypub - return if actor_json.nil? - - @account = ActivityPub::ProcessAccountService.new.call(@username, @domain, actor_json) - rescue Oj::ParseError - nil - end - def actor_url @actor_url ||= @webfinger.link('self').href end diff --git a/spec/services/resolve_account_service_spec.rb b/spec/services/resolve_account_service_spec.rb index 7a64f4161..cea942e39 100644 --- a/spec/services/resolve_account_service_spec.rb +++ b/spec/services/resolve_account_service_spec.rb @@ -53,6 +53,11 @@ RSpec.describe ResolveAccountService, type: :service do fail_occurred = false return_values = Concurrent::Array.new + # Preload classes that throw circular dependency errors in threads + Account + TagManager + DomainBlock + threads = Array.new(5) do Thread.new do true while wait_for_start -- cgit From 5d3feed191bcbe2769512119752b426108152fe9 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 10 Jul 2019 18:59:28 +0200 Subject: Refactor fetching of remote resources (#11251) --- app/helpers/jsonld_helper.rb | 47 ++++++----- app/lib/request.rb | 2 +- .../activitypub/fetch_remote_status_service.rb | 20 ++--- app/services/fetch_atom_service.rb | 93 --------------------- app/services/fetch_link_card_service.rb | 2 +- app/services/fetch_remote_account_service.rb | 2 +- app/services/fetch_remote_status_service.rb | 2 +- app/services/fetch_resource_service.rb | 68 +++++++++++++++ app/services/resolve_url_service.rb | 47 ++++------- app/workers/activitypub/delivery_worker.rb | 16 ++-- spec/services/fetch_atom_service_spec.rb | 96 ---------------------- spec/services/fetch_remote_account_service_spec.rb | 1 + spec/services/fetch_resource_service_spec.rb | 96 ++++++++++++++++++++++ spec/services/resolve_url_service_spec.rb | 44 ++-------- 14 files changed, 231 insertions(+), 305 deletions(-) delete mode 100644 app/services/fetch_atom_service.rb create mode 100644 app/services/fetch_resource_service.rb delete mode 100644 spec/services/fetch_atom_service_spec.rb create mode 100644 spec/services/fetch_resource_service_spec.rb (limited to 'app/helpers') diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb index 5b4011275..34a657e06 100644 --- a/app/helpers/jsonld_helper.rb +++ b/app/helpers/jsonld_helper.rb @@ -16,13 +16,15 @@ module JsonLdHelper # The url attribute can be a string, an array of strings, or an array of objects. # The objects could include a mimeType. Not-included mimeType means it's text/html. def url_to_href(value, preferred_type = nil) - single_value = if value.is_a?(Array) && !value.first.is_a?(String) - value.find { |link| preferred_type.nil? || ((link['mimeType'].presence || 'text/html') == preferred_type) } - elsif value.is_a?(Array) - value.first - else - value - end + single_value = begin + if value.is_a?(Array) && !value.first.is_a?(String) + value.find { |link| preferred_type.nil? || ((link['mimeType'].presence || 'text/html') == preferred_type) } + elsif value.is_a?(Array) + value.first + else + value + end + end if single_value.nil? || single_value.is_a?(String) single_value @@ -64,7 +66,9 @@ module JsonLdHelper def fetch_resource(uri, id, on_behalf_of = nil) unless id json = fetch_resource_without_id_validation(uri, on_behalf_of) + return unless json + uri = json['id'] end @@ -74,24 +78,26 @@ module JsonLdHelper def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false) build_request(uri, on_behalf_of).perform do |response| - unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error - raise Mastodon::UnexpectedResponseError, response - end + raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error + return body_to_json(response.body_with_limit) if response.code == 200 end + # If request failed, retry without doing it on behalf of a user return if on_behalf_of.nil? + build_request(uri).perform do |response| - unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error - raise Mastodon::UnexpectedResponseError, response - end + raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error + response.code == 200 ? body_to_json(response.body_with_limit) : nil end end def body_to_json(body, compare_id: nil) json = body.is_a?(String) ? Oj.load(body, mode: :strict) : body + return if compare_id.present? && json['id'] != compare_id + json rescue Oj::ParseError nil @@ -105,35 +111,34 @@ module JsonLdHelper end end - private - def response_successful?(response) (200...300).cover?(response.code) end def response_error_unsalvageable?(response) - (400...500).cover?(response.code) && response.code != 429 + response.code == 501 || ((400...500).cover?(response.code) && ![401, 408, 429].include?(response.code)) end def build_request(uri, on_behalf_of = nil) - request = Request.new(:get, uri) - request.on_behalf_of(on_behalf_of) if on_behalf_of - request.add_headers('Accept' => 'application/activity+json, application/ld+json') - request + Request.new(:get, uri).tap do |request| + request.on_behalf_of(on_behalf_of) if on_behalf_of + request.add_headers('Accept' => 'application/activity+json, application/ld+json') + end end def load_jsonld_context(url, _options = {}, &_block) json = Rails.cache.fetch("jsonld:context:#{url}", expires_in: 30.days, raw: true) do request = Request.new(:get, url) request.add_headers('Accept' => 'application/ld+json') - request.perform do |res| raise JSON::LD::JsonLdError::LoadingDocumentFailed unless res.code == 200 && res.mime_type == 'application/ld+json' + res.body_with_limit end end doc = JSON::LD::API::RemoteDocument.new(url, json) + block_given? ? yield(doc) : doc end end diff --git a/app/lib/request.rb b/app/lib/request.rb index 322457ad7..1fd3f5190 100644 --- a/app/lib/request.rb +++ b/app/lib/request.rb @@ -41,7 +41,7 @@ class Request end def on_behalf_of(account, key_id_format = :acct, sign_with: nil) - raise ArgumentError unless account.local? + raise ArgumentError, 'account must be local' unless account&.local? @account = account @keypair = sign_with.present? ? OpenSSL::PKey::RSA.new(sign_with) : @account.keypair diff --git a/app/services/activitypub/fetch_remote_status_service.rb b/app/services/activitypub/fetch_remote_status_service.rb index 469821032..cf4f62899 100644 --- a/app/services/activitypub/fetch_remote_status_service.rb +++ b/app/services/activitypub/fetch_remote_status_service.rb @@ -5,18 +5,18 @@ class ActivityPub::FetchRemoteStatusService < BaseService # Should be called when uri has already been checked for locality def call(uri, id: true, prefetched_body: nil, on_behalf_of: nil) - @json = if prefetched_body.nil? - fetch_resource(uri, id, on_behalf_of) - else - body_to_json(prefetched_body, compare_id: id ? uri : nil) - end + @json = begin + if prefetched_body.nil? + fetch_resource(uri, id, on_behalf_of) + else + body_to_json(prefetched_body, compare_id: id ? uri : nil) + end + end - return unless supported_context? && expected_type? - - return if actor_id.nil? || !trustworthy_attribution?(@json['id'], actor_id) + return if !(supported_context? && expected_type?) || actor_id.nil? || !trustworthy_attribution?(@json['id'], actor_id) actor = ActivityPub::TagManager.instance.uri_to_resource(actor_id, Account) - actor = ActivityPub::FetchRemoteAccountService.new.call(actor_id, id: true) if actor.nil? || needs_update(actor) + actor = ActivityPub::FetchRemoteAccountService.new.call(actor_id, id: true) if actor.nil? || needs_update?(actor) return if actor.nil? || actor.suspended? @@ -46,7 +46,7 @@ class ActivityPub::FetchRemoteStatusService < BaseService equals_or_includes_any?(@json['type'], ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES) end - def needs_update(actor) + def needs_update?(actor) actor.possibly_stale? end end diff --git a/app/services/fetch_atom_service.rb b/app/services/fetch_atom_service.rb deleted file mode 100644 index d6508a988..000000000 --- a/app/services/fetch_atom_service.rb +++ /dev/null @@ -1,93 +0,0 @@ -# frozen_string_literal: true - -class FetchAtomService < BaseService - include JsonLdHelper - - def call(url) - return if url.blank? - - result = process(url) - - # retry without ActivityPub - result ||= process(url) if @unsupported_activity - - result - rescue OpenSSL::SSL::SSLError => e - Rails.logger.debug "SSL error: #{e}" - nil - rescue HTTP::ConnectionError => e - Rails.logger.debug "HTTP ConnectionError: #{e}" - nil - end - - private - - def process(url, terminal = false) - @url = url - perform_request { |response| process_response(response, terminal) } - end - - def perform_request(&block) - accept = 'text/html' - accept = 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams", application/atom+xml, ' + accept unless @unsupported_activity - - Request.new(:get, @url).add_headers('Accept' => accept).perform(&block) - end - - def process_response(response, terminal = false) - return nil if response.code != 200 - - if response.mime_type == 'application/atom+xml' - [@url, { prefetched_body: response.body_with_limit }, :ostatus] - elsif ['application/activity+json', 'application/ld+json'].include?(response.mime_type) - body = response.body_with_limit - json = body_to_json(body) - if supported_context?(json) && equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) && json['inbox'].present? - [json['id'], { prefetched_body: body, id: true }, :activitypub] - elsif supported_context?(json) && expected_type?(json) - [json['id'], { prefetched_body: body, id: true }, :activitypub] - else - @unsupported_activity = true - nil - end - elsif !terminal - link_header = response['Link'] && parse_link_header(response) - - if link_header&.find_link(%w(rel alternate)) - process_link_headers(link_header) - elsif response.mime_type == 'text/html' - process_html(response) - end - end - end - - def expected_type?(json) - equals_or_includes_any?(json['type'], ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES) - end - - def process_html(response) - page = Nokogiri::HTML(response.body_with_limit) - - json_link = page.xpath('//link[@rel="alternate"]').find { |link| ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(link['type']) } - atom_link = page.xpath('//link[@rel="alternate"]').find { |link| link['type'] == 'application/atom+xml' } - - result ||= process(json_link['href'], terminal: true) unless json_link.nil? || @unsupported_activity - result ||= process(atom_link['href'], terminal: true) unless atom_link.nil? - - result - end - - def process_link_headers(link_header) - json_link = link_header.find_link(%w(rel alternate), %w(type application/activity+json)) || link_header.find_link(%w(rel alternate), ['type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"']) - atom_link = link_header.find_link(%w(rel alternate), %w(type application/atom+xml)) - - result ||= process(json_link.href, terminal: true) unless json_link.nil? || @unsupported_activity - result ||= process(atom_link.href, terminal: true) unless atom_link.nil? - - result - end - - def parse_link_header(response) - LinkHeader.parse(response['Link'].is_a?(Array) ? response['Link'].first : response['Link']) - end -end diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index 75fbd0e8c..4e75c370f 100644 --- a/app/services/fetch_link_card_service.rb +++ b/app/services/fetch_link_card_service.rb @@ -29,7 +29,7 @@ class FetchLinkCardService < BaseService end attach_card if @card&.persisted? - rescue HTTP::Error, Addressable::URI::InvalidURIError, Mastodon::HostValidationError, Mastodon::LengthValidationError => e + rescue HTTP::Error, OpenSSL::SSL::SSLError, Addressable::URI::InvalidURIError, Mastodon::HostValidationError, Mastodon::LengthValidationError => e Rails.logger.debug "Error fetching link #{@url}: #{e}" nil end diff --git a/app/services/fetch_remote_account_service.rb b/app/services/fetch_remote_account_service.rb index a7f95603d..3cd06e30f 100644 --- a/app/services/fetch_remote_account_service.rb +++ b/app/services/fetch_remote_account_service.rb @@ -3,7 +3,7 @@ class FetchRemoteAccountService < BaseService def call(url, prefetched_body = nil, protocol = :ostatus) if prefetched_body.nil? - resource_url, resource_options, protocol = FetchAtomService.new.call(url) + resource_url, resource_options, protocol = FetchResourceService.new.call(url) else resource_url = url resource_options = { prefetched_body: prefetched_body } diff --git a/app/services/fetch_remote_status_service.rb b/app/services/fetch_remote_status_service.rb index aac39dfd5..208dc7809 100644 --- a/app/services/fetch_remote_status_service.rb +++ b/app/services/fetch_remote_status_service.rb @@ -3,7 +3,7 @@ class FetchRemoteStatusService < BaseService def call(url, prefetched_body = nil, protocol = :ostatus) if prefetched_body.nil? - resource_url, resource_options, protocol = FetchAtomService.new.call(url) + resource_url, resource_options, protocol = FetchResourceService.new.call(url) else resource_url = url resource_options = { prefetched_body: prefetched_body } diff --git a/app/services/fetch_resource_service.rb b/app/services/fetch_resource_service.rb new file mode 100644 index 000000000..c0473f3ad --- /dev/null +++ b/app/services/fetch_resource_service.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +class FetchResourceService < BaseService + include JsonLdHelper + + ACCEPT_HEADER = 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams", text/html' + + def call(url) + return if url.blank? + + process(url) + rescue HTTP::Error, OpenSSL::SSL::SSLError, Addressable::URI::InvalidURIError, Mastodon::HostValidationError, Mastodon::LengthValidationError => e + Rails.logger.debug "Error fetching resource #{@url}: #{e}" + nil + end + + private + + def process(url, terminal = false) + @url = url + + perform_request { |response| process_response(response, terminal) } + end + + def perform_request(&block) + Request.new(:get, @url).add_headers('Accept' => ACCEPT_HEADER).perform(&block) + end + + def process_response(response, terminal = false) + return nil if response.code != 200 + + if ['application/activity+json', 'application/ld+json'].include?(response.mime_type) + body = response.body_with_limit + json = body_to_json(body) + + [json['id'], { prefetched_body: body, id: true }, :activitypub] if supported_context?(json) && (equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) || expected_type?(json)) + elsif !terminal + link_header = response['Link'] && parse_link_header(response) + + if link_header&.find_link(%w(rel alternate)) + process_link_headers(link_header) + elsif response.mime_type == 'text/html' + process_html(response) + end + end + end + + def expected_type?(json) + equals_or_includes_any?(json['type'], ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES) + end + + def process_html(response) + page = Nokogiri::HTML(response.body_with_limit) + json_link = page.xpath('//link[@rel="alternate"]').find { |link| ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(link['type']) } + + process(json_link['href'], terminal: true) unless json_link.nil? + end + + def process_link_headers(link_header) + json_link = link_header.find_link(%w(rel alternate), %w(type application/activity+json)) || link_header.find_link(%w(rel alternate), ['type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"']) + + process(json_link.href, terminal: true) unless json_link.nil? + end + + def parse_link_header(response) + LinkHeader.parse(response['Link'].is_a?(Array) ? response['Link'].first : response['Link']) + end +end diff --git a/app/services/resolve_url_service.rb b/app/services/resolve_url_service.rb index f941b489a..80381c16b 100644 --- a/app/services/resolve_url_service.rb +++ b/app/services/resolve_url_service.rb @@ -4,64 +4,49 @@ class ResolveURLService < BaseService include JsonLdHelper include Authorization - attr_reader :url - def call(url, on_behalf_of: nil) - @url = url + @url = url @on_behalf_of = on_behalf_of - return process_local_url if local_url? - - process_url unless fetched_atom_feed.nil? + if local_url? + process_local_url + elsif !fetched_resource.nil? + process_url + end end private def process_url if equals_or_includes_any?(type, ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES) - FetchRemoteAccountService.new.call(atom_url, body, protocol) + FetchRemoteAccountService.new.call(resource_url, body, protocol) elsif equals_or_includes_any?(type, ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES) - FetchRemoteStatusService.new.call(atom_url, body, protocol) + FetchRemoteStatusService.new.call(resource_url, body, protocol) end end - def fetched_atom_feed - @_fetched_atom_feed ||= FetchAtomService.new.call(url) + def fetched_resource + @fetched_resource ||= FetchResourceService.new.call(@url) end - def atom_url - fetched_atom_feed.first + def resource_url + fetched_resource.first end def body - fetched_atom_feed.second[:prefetched_body] + fetched_resource.second[:prefetched_body] end def protocol - fetched_atom_feed.third + fetched_resource.third end def type return json_data['type'] if protocol == :activitypub - - case xml_root - when 'feed' - 'Person' - when 'entry' - 'Note' - end end def json_data - @_json_data ||= body_to_json(body) - end - - def xml_root - xml_data.root.name - end - - def xml_data - @_xml_data ||= Nokogiri::XML(body, nil, 'utf-8') + @json_data ||= body_to_json(body) end def local_url? @@ -83,10 +68,10 @@ class ResolveURLService < BaseService def check_local_status(status) return if status.nil? + authorize_with @on_behalf_of, status, :show? status rescue Mastodon::NotPermittedError - # Do not disclose the existence of status the user is not authorized to see nil end end diff --git a/app/workers/activitypub/delivery_worker.rb b/app/workers/activitypub/delivery_worker.rb index 8b52b8e49..5457d9d4b 100644 --- a/app/workers/activitypub/delivery_worker.rb +++ b/app/workers/activitypub/delivery_worker.rb @@ -2,6 +2,7 @@ class ActivityPub::DeliveryWorker include Sidekiq::Worker + include JsonLdHelper STOPLIGHT_FAILURE_THRESHOLD = 10 STOPLIGHT_COOLDOWN = 60 @@ -32,9 +33,10 @@ class ActivityPub::DeliveryWorker private def build_request(http_client) - request = Request.new(:post, @inbox_url, body: @json, http_client: http_client) - request.on_behalf_of(@source_account, :uri, sign_with: @options[:sign_with]) - request.add_headers(HEADERS) + Request.new(:post, @inbox_url, body: @json, http_client: http_client).tap do |request| + request.on_behalf_of(@source_account, :uri, sign_with: @options[:sign_with]) + request.add_headers(HEADERS) + end end def perform_request @@ -53,14 +55,6 @@ class ActivityPub::DeliveryWorker .run end - def response_successful?(response) - (200...300).cover?(response.code) - end - - def response_error_unsalvageable?(response) - response.code == 501 || ((400...500).cover?(response.code) && ![401, 408, 429].include?(response.code)) - end - def failure_tracker @failure_tracker ||= DeliveryFailureTracker.new(@inbox_url) end diff --git a/spec/services/fetch_atom_service_spec.rb b/spec/services/fetch_atom_service_spec.rb deleted file mode 100644 index 495540004..000000000 --- a/spec/services/fetch_atom_service_spec.rb +++ /dev/null @@ -1,96 +0,0 @@ -require 'rails_helper' - -RSpec.describe FetchAtomService, type: :service do - describe '#call' do - let(:url) { 'http://example.com' } - subject { FetchAtomService.new.call(url) } - - context 'url is blank' do - let(:url) { '' } - it { is_expected.to be_nil } - end - - context 'request failed' do - before do - WebMock.stub_request(:get, url).to_return(status: 500, body: '', headers: {}) - end - - it { is_expected.to be_nil } - end - - context 'raise OpenSSL::SSL::SSLError' do - before do - allow(Request).to receive_message_chain(:new, :add_headers, :perform).and_raise(OpenSSL::SSL::SSLError) - end - - it 'output log and return nil' do - expect_any_instance_of(ActiveSupport::Logger).to receive(:debug).with('SSL error: OpenSSL::SSL::SSLError') - is_expected.to be_nil - end - end - - context 'raise HTTP::ConnectionError' do - before do - allow(Request).to receive_message_chain(:new, :add_headers, :perform).and_raise(HTTP::ConnectionError) - end - - it 'output log and return nil' do - expect_any_instance_of(ActiveSupport::Logger).to receive(:debug).with('HTTP ConnectionError: HTTP::ConnectionError') - is_expected.to be_nil - end - end - - context 'response success' do - let(:body) { '' } - let(:headers) { { 'Content-Type' => content_type } } - let(:json) { - { id: 1, - '@context': ActivityPub::TagManager::CONTEXT, - type: 'Note', - }.to_json - } - - before do - WebMock.stub_request(:get, url).to_return(status: 200, body: body, headers: headers) - end - - context 'content type is application/atom+xml' do - let(:content_type) { 'application/atom+xml' } - - it { is_expected.to eq [url, { :prefetched_body => "" }, :ostatus] } - end - - context 'content_type is activity+json' do - let(:content_type) { 'application/activity+json; charset=utf-8' } - let(:body) { json } - - it { is_expected.to eq [1, { prefetched_body: body, id: true }, :activitypub] } - end - - context 'content_type is ld+json with profile' do - let(:content_type) { 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' } - let(:body) { json } - - it { is_expected.to eq [1, { prefetched_body: body, id: true }, :activitypub] } - end - - before do - WebMock.stub_request(:get, url).to_return(status: 200, body: body, headers: headers) - WebMock.stub_request(:get, 'http://example.com/foo').to_return(status: 200, body: json, headers: { 'Content-Type' => 'application/activity+json' }) - end - - context 'has link header' do - let(:headers) { { 'Link' => '; rel="alternate"; type="application/activity+json"', } } - - it { is_expected.to eq [1, { prefetched_body: json, id: true }, :activitypub] } - end - - context 'content type is text/html' do - let(:content_type) { 'text/html' } - let(:body) { '' } - - it { is_expected.to eq [1, { prefetched_body: json, id: true }, :activitypub] } - end - end - end -end diff --git a/spec/services/fetch_remote_account_service_spec.rb b/spec/services/fetch_remote_account_service_spec.rb index 37e9910d4..ee7325be2 100644 --- a/spec/services/fetch_remote_account_service_spec.rb +++ b/spec/services/fetch_remote_account_service_spec.rb @@ -4,6 +4,7 @@ RSpec.describe FetchRemoteAccountService, type: :service do let(:url) { 'https://example.com/alice' } let(:prefetched_body) { nil } let(:protocol) { :ostatus } + subject { FetchRemoteAccountService.new.call(url, prefetched_body, protocol) } let(:actor) do diff --git a/spec/services/fetch_resource_service_spec.rb b/spec/services/fetch_resource_service_spec.rb new file mode 100644 index 000000000..17c192c44 --- /dev/null +++ b/spec/services/fetch_resource_service_spec.rb @@ -0,0 +1,96 @@ +require 'rails_helper' + +RSpec.describe FetchResourceService, type: :service do + let!(:representative) { Fabricate(:account) } + + describe '#call' do + let(:url) { 'http://example.com' } + subject { described_class.new.call(url) } + + context 'url is blank' do + let(:url) { '' } + it { is_expected.to be_nil } + end + + context 'request failed' do + before do + WebMock.stub_request(:get, url).to_return(status: 500, body: '', headers: {}) + end + + it { is_expected.to be_nil } + end + + context 'raise OpenSSL::SSL::SSLError' do + before do + allow(Request).to receive_message_chain(:new, :add_headers, :perform).and_raise(OpenSSL::SSL::SSLError) + end + + it 'return nil' do + is_expected.to be_nil + end + end + + context 'raise HTTP::ConnectionError' do + before do + allow(Request).to receive_message_chain(:new, :add_headers, :perform).and_raise(HTTP::ConnectionError) + end + + it 'return nil' do + is_expected.to be_nil + end + end + + context 'response success' do + let(:body) { '' } + let(:headers) { { 'Content-Type' => content_type } } + let(:json) { + { id: 1, + '@context': ActivityPub::TagManager::CONTEXT, + type: 'Note', + }.to_json + } + + before do + WebMock.stub_request(:get, url).to_return(status: 200, body: body, headers: headers) + end + + context 'content type is application/atom+xml' do + let(:content_type) { 'application/atom+xml' } + + it { is_expected.to eq nil } + end + + context 'content_type is activity+json' do + let(:content_type) { 'application/activity+json; charset=utf-8' } + let(:body) { json } + + it { is_expected.to eq [1, { prefetched_body: body, id: true }, :activitypub] } + end + + context 'content_type is ld+json with profile' do + let(:content_type) { 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' } + let(:body) { json } + + it { is_expected.to eq [1, { prefetched_body: body, id: true }, :activitypub] } + end + + before do + WebMock.stub_request(:get, url).to_return(status: 200, body: body, headers: headers) + WebMock.stub_request(:get, 'http://example.com/foo').to_return(status: 200, body: json, headers: { 'Content-Type' => 'application/activity+json' }) + end + + context 'has link header' do + let(:headers) { { 'Link' => '; rel="alternate"; type="application/activity+json"', } } + + it { is_expected.to eq [1, { prefetched_body: json, id: true }, :activitypub] } + end + + context 'content type is text/html' do + let(:content_type) { 'text/html' } + let(:body) { '' } + + it { is_expected.to eq [1, { prefetched_body: json, id: true }, :activitypub] } + end + end + end +end diff --git a/spec/services/resolve_url_service_spec.rb b/spec/services/resolve_url_service_spec.rb index 7bb5d1940..aa4204637 100644 --- a/spec/services/resolve_url_service_spec.rb +++ b/spec/services/resolve_url_service_spec.rb @@ -6,48 +6,14 @@ describe ResolveURLService, type: :service do subject { described_class.new } describe '#call' do - it 'returns nil when there is no atom url' do - url = 'http://example.com/missing-atom' + it 'returns nil when there is no resource url' do + url = 'http://example.com/missing-resource' service = double - allow(FetchAtomService).to receive(:new).and_return service - allow(service).to receive(:call).with(url).and_return(nil) - - result = subject.call(url) - expect(result).to be_nil - end - - it 'fetches remote accounts for feed types' do - url = 'http://example.com/atom-feed' - service = double - allow(FetchAtomService).to receive(:new).and_return service - feed_url = 'http://feed-url' - feed_content = 'contents' - allow(service).to receive(:call).with(url).and_return([feed_url, { prefetched_body: feed_content }]) - - account_service = double - allow(FetchRemoteAccountService).to receive(:new).and_return(account_service) - allow(account_service).to receive(:call) - - _result = subject.call(url) - expect(account_service).to have_received(:call).with(feed_url, feed_content, nil) - end - - it 'fetches remote statuses for entry types' do - url = 'http://example.com/atom-entry' - service = double - allow(FetchAtomService).to receive(:new).and_return service - feed_url = 'http://feed-url' - feed_content = 'contents' - allow(service).to receive(:call).with(url).and_return([feed_url, { prefetched_body: feed_content }]) - - account_service = double - allow(FetchRemoteStatusService).to receive(:new).and_return(account_service) - allow(account_service).to receive(:call) - - _result = subject.call(url) + allow(FetchResourceService).to receive(:new).and_return service + allow(service).to receive(:call).with(url).and_return(nil) - expect(account_service).to have_received(:call).with(feed_url, feed_content, nil) + expect(subject.call(url)).to be_nil end end end -- cgit From 4e8dcc5dbbf625b7268ed10d36122de985da6bdc Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 11 Jul 2019 14:49:55 +0200 Subject: Add HTTP signatures to all outgoing ActivityPub GET requests (#11284) --- app/helpers/jsonld_helper.rb | 13 ++--- app/lib/request.rb | 4 +- app/services/fetch_resource_service.rb | 2 +- .../concerns/signature_verification_spec.rb | 2 +- spec/services/fetch_remote_account_service_spec.rb | 1 + spec/services/fetch_resource_service_spec.rb | 61 +++++++++++++--------- 6 files changed, 43 insertions(+), 40 deletions(-) (limited to 'app/helpers') diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb index 34a657e06..83a5b2462 100644 --- a/app/helpers/jsonld_helper.rb +++ b/app/helpers/jsonld_helper.rb @@ -77,19 +77,12 @@ module JsonLdHelper end def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false) - build_request(uri, on_behalf_of).perform do |response| - raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error - - return body_to_json(response.body_with_limit) if response.code == 200 - end - - # If request failed, retry without doing it on behalf of a user - return if on_behalf_of.nil? + on_behalf_of ||= Account.representative - build_request(uri).perform do |response| + build_request(uri, on_behalf_of).perform do |response| raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error - response.code == 200 ? body_to_json(response.body_with_limit) : nil + body_to_json(response.body_with_limit) if response.code == 200 end end diff --git a/app/lib/request.rb b/app/lib/request.rb index 1fd3f5190..9d874fe2c 100644 --- a/app/lib/request.rb +++ b/app/lib/request.rb @@ -40,8 +40,8 @@ class Request set_digest! if options.key?(:body) end - def on_behalf_of(account, key_id_format = :acct, sign_with: nil) - raise ArgumentError, 'account must be local' unless account&.local? + def on_behalf_of(account, key_id_format = :uri, sign_with: nil) + raise ArgumentError, 'account must not be nil' if account.nil? @account = account @keypair = sign_with.present? ? OpenSSL::PKey::RSA.new(sign_with) : @account.keypair diff --git a/app/services/fetch_resource_service.rb b/app/services/fetch_resource_service.rb index c0473f3ad..3676d899d 100644 --- a/app/services/fetch_resource_service.rb +++ b/app/services/fetch_resource_service.rb @@ -23,7 +23,7 @@ class FetchResourceService < BaseService end def perform_request(&block) - Request.new(:get, @url).add_headers('Accept' => ACCEPT_HEADER).perform(&block) + Request.new(:get, @url).add_headers('Accept' => ACCEPT_HEADER).on_behalf_of(Account.representative).perform(&block) end def process_response(response, terminal = false) diff --git a/spec/controllers/concerns/signature_verification_spec.rb b/spec/controllers/concerns/signature_verification_spec.rb index 720690097..1fa19f54d 100644 --- a/spec/controllers/concerns/signature_verification_spec.rb +++ b/spec/controllers/concerns/signature_verification_spec.rb @@ -38,7 +38,7 @@ describe ApplicationController, type: :controller do end context 'with signature header' do - let!(:author) { Fabricate(:account) } + let!(:author) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/actor') } context 'without body' do before do diff --git a/spec/services/fetch_remote_account_service_spec.rb b/spec/services/fetch_remote_account_service_spec.rb index ee7325be2..b37445861 100644 --- a/spec/services/fetch_remote_account_service_spec.rb +++ b/spec/services/fetch_remote_account_service_spec.rb @@ -4,6 +4,7 @@ RSpec.describe FetchRemoteAccountService, type: :service do let(:url) { 'https://example.com/alice' } let(:prefetched_body) { nil } let(:protocol) { :ostatus } + let!(:representative) { Fabricate(:account) } subject { FetchRemoteAccountService.new.call(url, prefetched_body, protocol) } diff --git a/spec/services/fetch_resource_service_spec.rb b/spec/services/fetch_resource_service_spec.rb index 17c192c44..98630966b 100644 --- a/spec/services/fetch_resource_service_spec.rb +++ b/spec/services/fetch_resource_service_spec.rb @@ -5,69 +5,78 @@ RSpec.describe FetchResourceService, type: :service do describe '#call' do let(:url) { 'http://example.com' } + subject { described_class.new.call(url) } - context 'url is blank' do + context 'with blank url' do let(:url) { '' } it { is_expected.to be_nil } end - context 'request failed' do + context 'when request fails' do before do - WebMock.stub_request(:get, url).to_return(status: 500, body: '', headers: {}) + stub_request(:get, url).to_return(status: 500, body: '', headers: {}) end it { is_expected.to be_nil } end - context 'raise OpenSSL::SSL::SSLError' do + context 'when OpenSSL::SSL::SSLError is raised' do before do - allow(Request).to receive_message_chain(:new, :add_headers, :perform).and_raise(OpenSSL::SSL::SSLError) + allow(Request).to receive_message_chain(:new, :add_headers, :on_behalf_of, :perform).and_raise(OpenSSL::SSL::SSLError) end - it 'return nil' do - is_expected.to be_nil - end + it { is_expected.to be_nil } end - context 'raise HTTP::ConnectionError' do + context 'when HTTP::ConnectionError is raised' do before do - allow(Request).to receive_message_chain(:new, :add_headers, :perform).and_raise(HTTP::ConnectionError) + allow(Request).to receive_message_chain(:new, :add_headers, :on_behalf_of, :perform).and_raise(HTTP::ConnectionError) end - it 'return nil' do - is_expected.to be_nil - end + it { is_expected.to be_nil } end - context 'response success' do + context 'when request succeeds' do let(:body) { '' } - let(:headers) { { 'Content-Type' => content_type } } - let(:json) { - { id: 1, + + let(:content_type) { 'application/json' } + + let(:headers) do + { 'Content-Type' => content_type } + end + + let(:json) do + { + id: 1, '@context': ActivityPub::TagManager::CONTEXT, type: 'Note', }.to_json - } + end before do - WebMock.stub_request(:get, url).to_return(status: 200, body: body, headers: headers) + stub_request(:get, url).to_return(status: 200, body: body, headers: headers) + end + + it 'signs request' do + subject + expect(a_request(:get, url).with(headers: { 'Signature' => /keyId="#{Regexp.escape(ActivityPub::TagManager.instance.uri_for(representative) + '#main-key')}"/ })).to have_been_made end - context 'content type is application/atom+xml' do + context 'when content type is application/atom+xml' do let(:content_type) { 'application/atom+xml' } it { is_expected.to eq nil } end - context 'content_type is activity+json' do + context 'when content type is activity+json' do let(:content_type) { 'application/activity+json; charset=utf-8' } let(:body) { json } it { is_expected.to eq [1, { prefetched_body: body, id: true }, :activitypub] } end - context 'content_type is ld+json with profile' do + context 'when content type is ld+json with profile' do let(:content_type) { 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' } let(:body) { json } @@ -75,17 +84,17 @@ RSpec.describe FetchResourceService, type: :service do end before do - WebMock.stub_request(:get, url).to_return(status: 200, body: body, headers: headers) - WebMock.stub_request(:get, 'http://example.com/foo').to_return(status: 200, body: json, headers: { 'Content-Type' => 'application/activity+json' }) + stub_request(:get, url).to_return(status: 200, body: body, headers: headers) + stub_request(:get, 'http://example.com/foo').to_return(status: 200, body: json, headers: { 'Content-Type' => 'application/activity+json' }) end - context 'has link header' do + context 'when link header is present' do let(:headers) { { 'Link' => '; rel="alternate"; type="application/activity+json"', } } it { is_expected.to eq [1, { prefetched_body: json, id: true }, :activitypub] } end - context 'content type is text/html' do + context 'when content type is text/html' do let(:content_type) { 'text/html' } let(:body) { '' } -- cgit From 15de24a425cb07efd58db5299b043e4110fa138f Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sun, 28 Jul 2019 13:48:43 +0200 Subject: Bump json-ld-preloaded from 3.0.2 to 3.0.3 (#11316) * Bump json-ld-preloaded from 3.0.2 to 3.0.3 Bumps [json-ld-preloaded](https://github.com/ruby-rdf/json-ld-preloaded) from 3.0.2 to 3.0.3. - [Release notes](https://github.com/ruby-rdf/json-ld-preloaded/releases) - [Commits](https://github.com/ruby-rdf/json-ld-preloaded/compare/3.0.2...3.0.3) Signed-off-by: dependabot-preview[bot] * use json-ld edge --- Gemfile | 2 +- Gemfile.lock | 23 +++++++++++++++++------ app/helpers/jsonld_helper.rb | 2 +- lib/json_ld/security.rb | 5 +++-- 4 files changed, 22 insertions(+), 10 deletions(-) (limited to 'app/helpers') diff --git a/Gemfile b/Gemfile index 96eb44af7..04ebc4cd8 100644 --- a/Gemfile +++ b/Gemfile @@ -92,7 +92,7 @@ gem 'tzinfo-data', '~> 1.2019' gem 'webpacker', '~> 4.0' gem 'webpush' -gem 'json-ld', '~> 3.0' +gem 'json-ld', git: 'https://github.com/ruby-rdf/json-ld.git', ref: '345b7a5733308af827e8491d284dbafa9128d7a2' gem 'json-ld-preloaded', '~> 3.0' gem 'rdf-normalize', '~> 0.3' diff --git a/Gemfile.lock b/Gemfile.lock index 5adae6f9c..8da261639 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,6 +5,19 @@ GIT specs: posix-spawn (0.3.13) +GIT + remote: https://github.com/ruby-rdf/json-ld.git + revision: 345b7a5733308af827e8491d284dbafa9128d7a2 + ref: 345b7a5733308af827e8491d284dbafa9128d7a2 + specs: + json-ld (3.0.2) + htmlentities (~> 4.3) + json-canonicalization (~> 0.1) + link_header (~> 0.0, >= 0.0.8) + multi_json (~> 1.13) + rack (>= 1.6, < 3.0) + rdf (~> 3.0, >= 3.0.8) + GIT remote: https://github.com/tmm1/http_parser.rb revision: 54b17ba8c7d8d20a16dfc65d1775241833219cf2 @@ -297,10 +310,8 @@ GEM jaro_winkler (1.5.3) jmespath (1.4.0) json (2.2.0) - json-ld (3.0.2) - multi_json (~> 1.12) - rdf (>= 2.2.8, < 4.0) - json-ld-preloaded (3.0.2) + json-canonicalization (0.1.0) + json-ld-preloaded (3.0.3) json-ld (~> 3.0) multi_json (~> 1.12) rdf (~> 3.0) @@ -479,7 +490,7 @@ GEM thor (>= 0.19.0, < 2.0) rainbow (3.0.0) rake (12.3.2) - rdf (3.0.9) + rdf (3.0.12) hamster (~> 3.0) link_header (~> 0.0, >= 0.0.8) rdf-normalize (0.3.3) @@ -700,7 +711,7 @@ DEPENDENCIES i18n-tasks (~> 0.9) idn-ruby iso-639 - json-ld (~> 3.0) + json-ld! json-ld-preloaded (~> 3.0) kaminari (~> 1.1) letter_opener (~> 1.7) diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb index 83a5b2462..1c473efa3 100644 --- a/app/helpers/jsonld_helper.rb +++ b/app/helpers/jsonld_helper.rb @@ -130,7 +130,7 @@ module JsonLdHelper end end - doc = JSON::LD::API::RemoteDocument.new(url, json) + doc = JSON::LD::API::RemoteDocument.new(json, documentUrl: url) block_given? ? yield(doc) : doc end diff --git a/lib/json_ld/security.rb b/lib/json_ld/security.rb index 1230206f0..a6fbce95f 100644 --- a/lib/json_ld/security.rb +++ b/lib/json_ld/security.rb @@ -1,9 +1,9 @@ # -*- encoding: utf-8 -*- # frozen_string_literal: true -# This file generated automatically from https://w3id.org/security/v1 +# This file generated automatically from http://w3id.org/security/v1 require 'json/ld' class JSON::LD::Context - add_preloaded("https://w3id.org/security/v1") do + add_preloaded("http://w3id.org/security/v1") do new(processingMode: "json-ld-1.0", term_definitions: { "CryptographicKey" => TermDefinition.new("CryptographicKey", id: "https://w3id.org/security#Key", simple: true), "EcdsaKoblitzSignature2016" => TermDefinition.new("EcdsaKoblitzSignature2016", id: "https://w3id.org/security#EcdsaKoblitzSignature2016", simple: true), @@ -47,4 +47,5 @@ class JSON::LD::Context "xsd" => TermDefinition.new("xsd", id: "http://www.w3.org/2001/XMLSchema#", simple: true, prefix: true) }) end + alias_preloaded("https://w3id.org/security/v1", "http://w3id.org/security/v1") end -- cgit From 24552b5160a5090e7d6056fb69a209aa48fe4fce Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 30 Jul 2019 11:10:46 +0200 Subject: Add whitelist mode (#11291) --- app/controllers/about_controller.rb | 5 +++ app/controllers/activitypub/base_controller.rb | 2 ++ app/controllers/activitypub/inboxes_controller.rb | 2 +- app/controllers/admin/domain_allows_controller.rb | 40 ++++++++++++++++++++++ app/controllers/admin/instances_controller.rb | 28 +++++++++++++-- app/controllers/api/base_controller.rb | 9 +++++ app/controllers/api/v1/accounts_controller.rb | 2 ++ app/controllers/api/v1/apps_controller.rb | 2 ++ .../api/v1/instances/activity_controller.rb | 3 +- .../api/v1/instances/peers_controller.rb | 3 +- app/controllers/api/v1/instances_controller.rb | 1 + app/controllers/application_controller.rb | 4 ++- app/controllers/concerns/account_owned_concern.rb | 1 + app/controllers/directories_controller.rb | 5 +-- app/controllers/home_controller.rb | 2 +- app/controllers/media_controller.rb | 1 + app/controllers/media_proxy_controller.rb | 2 ++ app/controllers/public_timelines_controller.rb | 5 +-- app/controllers/remote_interaction_controller.rb | 1 + app/controllers/tags_controller.rb | 1 + app/helpers/domain_control_helper.rb | 10 +++++- app/models/domain_allow.rb | 33 ++++++++++++++++++ app/models/instance.rb | 3 +- app/models/instance_filter.rb | 4 +++ app/policies/domain_allow_policy.rb | 11 ++++++ app/services/concerns/payloadable.rb | 2 +- app/services/unallow_domain_service.rb | 11 ++++++ app/views/admin/domain_allows/new.html.haml | 14 ++++++++ app/views/admin/instances/index.html.haml | 35 ++++++++++++------- app/views/admin/instances/show.html.haml | 4 ++- app/views/admin/settings/edit.html.haml | 28 ++++++++------- app/views/auth/registrations/new.html.haml | 2 +- app/views/layouts/public.html.haml | 9 +++-- config/initializers/2_whitelist_mode.rb | 5 +++ config/locales/en.yml | 7 ++++ config/locales/simple_form.en.yml | 2 ++ config/navigation.rb | 2 +- config/routes.rb | 1 + db/migrate/20190705002136_create_domain_allows.rb | 9 +++++ db/schema.rb | 9 ++++- lib/mastodon/domains_cli.rb | 22 ++++++++++-- spec/fabricators/domain_allow_fabricator.rb | 3 ++ spec/models/domain_allow_spec.rb | 5 +++ streaming/index.js | 5 +-- 44 files changed, 302 insertions(+), 53 deletions(-) create mode 100644 app/controllers/admin/domain_allows_controller.rb create mode 100644 app/models/domain_allow.rb create mode 100644 app/policies/domain_allow_policy.rb create mode 100644 app/services/unallow_domain_service.rb create mode 100644 app/views/admin/domain_allows/new.html.haml create mode 100644 config/initializers/2_whitelist_mode.rb create mode 100644 db/migrate/20190705002136_create_domain_allows.rb create mode 100644 spec/fabricators/domain_allow_fabricator.rb create mode 100644 spec/models/domain_allow_spec.rb (limited to 'app/helpers') diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 31cf17710..d276e8fe5 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -3,6 +3,7 @@ class AboutController < ApplicationController layout 'public' + before_action :require_open_federation!, only: [:show, :more] before_action :set_body_classes, only: :show before_action :set_instance_presenter before_action :set_expires_in @@ -19,6 +20,10 @@ class AboutController < ApplicationController private + def require_open_federation! + not_found if whitelist_mode? + end + def new_user User.new.tap do |user| user.build_account diff --git a/app/controllers/activitypub/base_controller.rb b/app/controllers/activitypub/base_controller.rb index a3b5c4dfa..0c2591e97 100644 --- a/app/controllers/activitypub/base_controller.rb +++ b/app/controllers/activitypub/base_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class ActivityPub::BaseController < Api::BaseController + skip_before_action :require_authenticated_user! + private def set_cache_headers diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb index 7cfd9a25e..bcfc1e6d4 100644 --- a/app/controllers/activitypub/inboxes_controller.rb +++ b/app/controllers/activitypub/inboxes_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class ActivityPub::InboxesController < Api::BaseController +class ActivityPub::InboxesController < ActivityPub::BaseController include SignatureVerification include JsonLdHelper include AccountOwnedConcern diff --git a/app/controllers/admin/domain_allows_controller.rb b/app/controllers/admin/domain_allows_controller.rb new file mode 100644 index 000000000..31be1978b --- /dev/null +++ b/app/controllers/admin/domain_allows_controller.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +class Admin::DomainAllowsController < Admin::BaseController + before_action :set_domain_allow, only: [:destroy] + + def new + authorize :domain_allow, :create? + + @domain_allow = DomainAllow.new(domain: params[:_domain]) + end + + def create + authorize :domain_allow, :create? + + @domain_allow = DomainAllow.new(resource_params) + + if @domain_allow.save + log_action :create, @domain_allow + redirect_to admin_instances_path, notice: I18n.t('admin.domain_allows.created_msg') + else + render :new + end + end + + def destroy + authorize @domain_allow, :destroy? + UnallowDomainService.new.call(@domain_allow) + redirect_to admin_instances_path, notice: I18n.t('admin.domain_allows.destroyed_msg') + end + + private + + def set_domain_allow + @domain_allow = DomainAllow.find(params[:id]) + end + + def resource_params + params.require(:domain_allow).permit(:domain) + end +end diff --git a/app/controllers/admin/instances_controller.rb b/app/controllers/admin/instances_controller.rb index 7888e844f..d4f201807 100644 --- a/app/controllers/admin/instances_controller.rb +++ b/app/controllers/admin/instances_controller.rb @@ -2,6 +2,10 @@ module Admin class InstancesController < BaseController + before_action :set_domain_block, only: :show + before_action :set_domain_allow, only: :show + before_action :set_instance, only: :show + def index authorize :instance, :index? @@ -11,20 +15,38 @@ module Admin def show authorize :instance, :show? - @instance = Instance.new(Account.by_domain_accounts.find_by(domain: params[:id]) || DomainBlock.find_by!(domain: params[:id])) @following_count = Follow.where(account: Account.where(domain: params[:id])).count @followers_count = Follow.where(target_account: Account.where(domain: params[:id])).count @reports_count = Report.where(target_account: Account.where(domain: params[:id])).count @blocks_count = Block.where(target_account: Account.where(domain: params[:id])).count @available = DeliveryFailureTracker.available?(Account.select(:shared_inbox_url).where(domain: params[:id]).first&.shared_inbox_url) @media_storage = MediaAttachment.where(account: Account.where(domain: params[:id])).sum(:file_file_size) - @domain_block = DomainBlock.rule_for(params[:id]) end private + def set_domain_block + @domain_block = DomainBlock.rule_for(params[:id]) + end + + def set_domain_allow + @domain_allow = DomainAllow.rule_for(params[:id]) + end + + def set_instance + resource = Account.by_domain_accounts.find_by(domain: params[:id]) + resource ||= @domain_block + resource ||= @domain_allow + + if resource + @instance = Instance.new(resource) + else + not_found + end + end + def filtered_instances - InstanceFilter.new(filter_params).results + InstanceFilter.new(whitelist_mode? ? { allowed: true } : filter_params).results end def paginated_instances diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 6f33a1ea9..109e38ffa 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -9,6 +9,7 @@ class Api::BaseController < ApplicationController skip_before_action :store_current_location skip_before_action :require_functional! + before_action :require_authenticated_user!, if: :disallow_unauthenticated_api_access? before_action :set_cache_headers protect_from_forgery with: :null_session @@ -69,6 +70,10 @@ class Api::BaseController < ApplicationController nil end + def require_authenticated_user! + render json: { error: 'This API requires an authenticated user' }, status: 401 unless current_user + end + def require_user! if !current_user render json: { error: 'This method requires an authenticated user' }, status: 422 @@ -94,4 +99,8 @@ class Api::BaseController < ApplicationController def set_cache_headers response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate' end + + def disallow_unauthenticated_api_access? + authorized_fetch_mode? + end end diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index b0c62778e..b306e8e8c 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -12,6 +12,8 @@ class Api::V1::AccountsController < Api::BaseController before_action :check_account_suspension, only: [:show] before_action :check_enabled_registrations, only: [:create] + skip_before_action :require_authenticated_user!, only: :create + respond_to :json def show diff --git a/app/controllers/api/v1/apps_controller.rb b/app/controllers/api/v1/apps_controller.rb index e9f7a7291..97177547a 100644 --- a/app/controllers/api/v1/apps_controller.rb +++ b/app/controllers/api/v1/apps_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Api::V1::AppsController < Api::BaseController + skip_before_action :require_authenticated_user! + def create @app = Doorkeeper::Application.create!(application_options) render json: @app, serializer: REST::ApplicationSerializer diff --git a/app/controllers/api/v1/instances/activity_controller.rb b/app/controllers/api/v1/instances/activity_controller.rb index d0080c5c2..4fb5a69d8 100644 --- a/app/controllers/api/v1/instances/activity_controller.rb +++ b/app/controllers/api/v1/instances/activity_controller.rb @@ -2,6 +2,7 @@ class Api::V1::Instances::ActivityController < Api::BaseController before_action :require_enabled_api! + skip_before_action :set_cache_headers respond_to :json @@ -33,6 +34,6 @@ class Api::V1::Instances::ActivityController < Api::BaseController end def require_enabled_api! - head 404 unless Setting.activity_api_enabled + head 404 unless Setting.activity_api_enabled && !whitelist_mode? end end diff --git a/app/controllers/api/v1/instances/peers_controller.rb b/app/controllers/api/v1/instances/peers_controller.rb index 450e6502f..75c3cb4ba 100644 --- a/app/controllers/api/v1/instances/peers_controller.rb +++ b/app/controllers/api/v1/instances/peers_controller.rb @@ -2,6 +2,7 @@ class Api::V1::Instances::PeersController < Api::BaseController before_action :require_enabled_api! + skip_before_action :set_cache_headers respond_to :json @@ -14,6 +15,6 @@ class Api::V1::Instances::PeersController < Api::BaseController private def require_enabled_api! - head 404 unless Setting.peers_api_enabled + head 404 unless Setting.peers_api_enabled && !whitelist_mode? end end diff --git a/app/controllers/api/v1/instances_controller.rb b/app/controllers/api/v1/instances_controller.rb index 93e4f0003..8d8231423 100644 --- a/app/controllers/api/v1/instances_controller.rb +++ b/app/controllers/api/v1/instances_controller.rb @@ -2,6 +2,7 @@ class Api::V1::InstancesController < Api::BaseController respond_to :json + skip_before_action :set_cache_headers def show diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 41ce1a0ca..0d3913ee0 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -11,12 +11,14 @@ class ApplicationController < ActionController::Base include UserTrackingConcern include SessionTrackingConcern include CacheConcern + include DomainControlHelper helper_method :current_account helper_method :current_session helper_method :current_theme helper_method :single_user_mode? helper_method :use_seamless_external_login? + helper_method :whitelist_mode? rescue_from ActionController::RoutingError, with: :not_found rescue_from ActiveRecord::RecordNotFound, with: :not_found @@ -38,7 +40,7 @@ class ApplicationController < ActionController::Base end def authorized_fetch_mode? - ENV['AUTHORIZED_FETCH'] == 'true' + ENV['AUTHORIZED_FETCH'] == 'true' || Rails.configuration.x.whitelist_mode end def public_fetch_mode? diff --git a/app/controllers/concerns/account_owned_concern.rb b/app/controllers/concerns/account_owned_concern.rb index 99c240fe9..460f71f65 100644 --- a/app/controllers/concerns/account_owned_concern.rb +++ b/app/controllers/concerns/account_owned_concern.rb @@ -4,6 +4,7 @@ module AccountOwnedConcern extend ActiveSupport::Concern included do + before_action :authenticate_user!, if: -> { whitelist_mode? && request.format != :json } before_action :set_account, if: :account_required? before_action :check_account_approval, if: :account_required? before_action :check_account_suspension, if: :account_required? diff --git a/app/controllers/directories_controller.rb b/app/controllers/directories_controller.rb index 594907674..d2ef76f06 100644 --- a/app/controllers/directories_controller.rb +++ b/app/controllers/directories_controller.rb @@ -3,7 +3,8 @@ class DirectoriesController < ApplicationController layout 'public' - before_action :check_enabled + before_action :authenticate_user!, if: :whitelist_mode? + before_action :require_enabled! before_action :set_instance_presenter before_action :set_tag, only: :show before_action :set_tags @@ -19,7 +20,7 @@ class DirectoriesController < ApplicationController private - def check_enabled + def require_enabled! return not_found unless Setting.profile_directory end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 42493cd78..22d507e77 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -55,7 +55,7 @@ class HomeController < ApplicationController end def default_redirect_path - if request.path.start_with?('/web') + if request.path.start_with?('/web') || whitelist_mode? new_user_session_path elsif single_user_mode? short_account_path(Account.local.without_suspended.where('id > 0').first) diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb index b3b7519a1..1f693de32 100644 --- a/app/controllers/media_controller.rb +++ b/app/controllers/media_controller.rb @@ -5,6 +5,7 @@ class MediaController < ApplicationController skip_before_action :store_current_location + before_action :authenticate_user!, if: :whitelist_mode? before_action :set_media_attachment before_action :verify_permitted_status! before_action :check_playable, only: :player diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb index 8fc18dd06..8da6c6fe0 100644 --- a/app/controllers/media_proxy_controller.rb +++ b/app/controllers/media_proxy_controller.rb @@ -5,6 +5,8 @@ class MediaProxyController < ApplicationController skip_before_action :store_current_location + before_action :authenticate_user!, if: :whitelist_mode? + def show RedisLock.acquire(lock_options) do |lock| if lock.acquired? diff --git a/app/controllers/public_timelines_controller.rb b/app/controllers/public_timelines_controller.rb index 23506b990..324bdc508 100644 --- a/app/controllers/public_timelines_controller.rb +++ b/app/controllers/public_timelines_controller.rb @@ -3,7 +3,8 @@ class PublicTimelinesController < ApplicationController layout 'public' - before_action :check_enabled + before_action :authenticate_user!, if: :whitelist_mode? + before_action :require_enabled! before_action :set_body_classes before_action :set_instance_presenter @@ -16,7 +17,7 @@ class PublicTimelinesController < ApplicationController private - def check_enabled + def require_enabled! not_found unless Setting.timeline_preview end diff --git a/app/controllers/remote_interaction_controller.rb b/app/controllers/remote_interaction_controller.rb index cc6993c52..fa742fb0a 100644 --- a/app/controllers/remote_interaction_controller.rb +++ b/app/controllers/remote_interaction_controller.rb @@ -5,6 +5,7 @@ class RemoteInteractionController < ApplicationController layout 'modal' + before_action :authenticate_user!, if: :whitelist_mode? before_action :set_interaction_type before_action :set_status before_action :set_body_classes diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index d08e5a61a..3cd2d9e20 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -8,6 +8,7 @@ class TagsController < ApplicationController layout 'public' before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? } + before_action :authenticate_user!, if: :whitelist_mode? before_action :set_tag before_action :set_body_classes before_action :set_instance_presenter diff --git a/app/helpers/domain_control_helper.rb b/app/helpers/domain_control_helper.rb index efd328f81..067b2c2cd 100644 --- a/app/helpers/domain_control_helper.rb +++ b/app/helpers/domain_control_helper.rb @@ -12,6 +12,14 @@ module DomainControlHelper end end - DomainBlock.blocked?(domain) + if whitelist_mode? + !DomainAllow.allowed?(domain) + else + DomainBlock.blocked?(domain) + end + end + + def whitelist_mode? + Rails.configuration.x.whitelist_mode end end diff --git a/app/models/domain_allow.rb b/app/models/domain_allow.rb new file mode 100644 index 000000000..85018b636 --- /dev/null +++ b/app/models/domain_allow.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: domain_allows +# +# id :bigint(8) not null, primary key +# domain :string default(""), not null +# created_at :datetime not null +# updated_at :datetime not null +# + +class DomainAllow < ApplicationRecord + include DomainNormalizable + + validates :domain, presence: true, uniqueness: true + + scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) } + + class << self + def allowed?(domain) + !rule_for(domain).nil? + end + + def rule_for(domain) + return if domain.blank? + + uri = Addressable::URI.new.tap { |u| u.host = domain.gsub(/[\/]/, '') } + + find_by(domain: uri.normalized_host) + end + end +end diff --git a/app/models/instance.rb b/app/models/instance.rb index 797a191e0..3c740f8a2 100644 --- a/app/models/instance.rb +++ b/app/models/instance.rb @@ -7,8 +7,9 @@ class Instance def initialize(resource) @domain = resource.domain - @accounts_count = resource.is_a?(DomainBlock) ? nil : resource.accounts_count + @accounts_count = resource.respond_to?(:accounts_count) ? resource.accounts_count : nil @domain_block = resource.is_a?(DomainBlock) ? resource : DomainBlock.rule_for(domain) + @domain_allow = resource.is_a?(DomainAllow) ? resource : DomainAllow.rule_for(domain) end def countable? diff --git a/app/models/instance_filter.rb b/app/models/instance_filter.rb index 848fff53e..8bfab826d 100644 --- a/app/models/instance_filter.rb +++ b/app/models/instance_filter.rb @@ -12,6 +12,10 @@ class InstanceFilter scope = DomainBlock scope = scope.matches_domain(params[:by_domain]) if params[:by_domain].present? scope.order(id: :desc) + elsif params[:allowed].present? + scope = DomainAllow + scope = scope.matches_domain(params[:by_domain]) if params[:by_domain].present? + scope.order(id: :desc) else scope = Account.remote scope = scope.matches_domain(params[:by_domain]) if params[:by_domain].present? diff --git a/app/policies/domain_allow_policy.rb b/app/policies/domain_allow_policy.rb new file mode 100644 index 000000000..5030453bb --- /dev/null +++ b/app/policies/domain_allow_policy.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class DomainAllowPolicy < ApplicationPolicy + def create? + admin? + end + + def destroy? + admin? + end +end diff --git a/app/services/concerns/payloadable.rb b/app/services/concerns/payloadable.rb index 953740faa..7f9f21c4b 100644 --- a/app/services/concerns/payloadable.rb +++ b/app/services/concerns/payloadable.rb @@ -14,6 +14,6 @@ module Payloadable end def signing_enabled? - ENV['AUTHORIZED_FETCH'] != 'true' + ENV['AUTHORIZED_FETCH'] != 'true' && !Rails.configuration.x.whitelist_mode end end diff --git a/app/services/unallow_domain_service.rb b/app/services/unallow_domain_service.rb new file mode 100644 index 000000000..d4387c1a1 --- /dev/null +++ b/app/services/unallow_domain_service.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class UnallowDomainService < BaseService + def call(domain_allow) + Account.where(domain: domain_allow.domain).find_each do |account| + SuspendAccountService.new.call(account, destroy: true) + end + + domain_allow.destroy + end +end diff --git a/app/views/admin/domain_allows/new.html.haml b/app/views/admin/domain_allows/new.html.haml new file mode 100644 index 000000000..52599857a --- /dev/null +++ b/app/views/admin/domain_allows/new.html.haml @@ -0,0 +1,14 @@ +- content_for :header_tags do + = javascript_pack_tag 'admin', integrity: true, async: true, crossorigin: 'anonymous' + +- content_for :page_title do + = t('admin.domain_allows.add_new') + += simple_form_for @domain_allow, url: admin_domain_allows_path do |f| + = render 'shared/error_messages', object: @domain_allow + + .fields-group + = f.input :domain, wrapper: :with_label, label: t('admin.domain_blocks.domain'), required: true + + .actions + = f.button :button, t('admin.domain_allows.add_new'), type: :submit diff --git a/app/views/admin/instances/index.html.haml b/app/views/admin/instances/index.html.haml index 61e578409..982dc5035 100644 --- a/app/views/admin/instances/index.html.haml +++ b/app/views/admin/instances/index.html.haml @@ -6,24 +6,30 @@ %strong= t('admin.instances.moderation.title') %ul %li= filter_link_to t('admin.instances.moderation.all'), limited: nil - %li= filter_link_to t('admin.instances.moderation.limited'), limited: '1' + + - unless whitelist_mode? + %li= filter_link_to t('admin.instances.moderation.limited'), limited: '1' %div{ style: 'flex: 1 1 auto; text-align: right' } - = link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path, class: 'button' + - if whitelist_mode? + = link_to t('admin.domain_allows.add_new'), new_admin_domain_allow_path, class: 'button' + - else + = link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path, class: 'button' -= form_tag admin_instances_url, method: 'GET', class: 'simple_form' do - .fields-group - - Admin::FilterHelper::INSTANCES_FILTERS.each do |key| - - if params[key].present? - = hidden_field_tag key, params[key] +- unless whitelist_mode? + = form_tag admin_instances_url, method: 'GET', class: 'simple_form' do + .fields-group + - Admin::FilterHelper::INSTANCES_FILTERS.each do |key| + - if params[key].present? + = hidden_field_tag key, params[key] - - %i(by_domain).each do |key| - .input.string.optional - = text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.instances.#{key}") + - %i(by_domain).each do |key| + .input.string.optional + = text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.instances.#{key}") - .actions - %button= t('admin.accounts.search') - = link_to t('admin.accounts.reset'), admin_instances_path, class: 'button negative' + .actions + %button= t('admin.accounts.search') + = link_to t('admin.accounts.reset'), admin_instances_path, class: 'button negative' %hr.spacer/ @@ -47,8 +53,11 @@ - unless first_item • = t('admin.domain_blocks.rejecting_reports') + - elsif whitelist_mode? + = t('admin.accounts.whitelisted') - else = t('admin.accounts.no_limits_imposed') - if instance.countable? .trends__item__current{ title: t('admin.instances.known_accounts', count: instance.accounts_count) }= number_to_human instance.accounts_count, strip_insignificant_zeros: true + = paginate paginated_instances diff --git a/app/views/admin/instances/show.html.haml b/app/views/admin/instances/show.html.haml index c7992a490..fbb49ba02 100644 --- a/app/views/admin/instances/show.html.haml +++ b/app/views/admin/instances/show.html.haml @@ -38,7 +38,9 @@ = link_to t('admin.accounts.title'), admin_accounts_path(remote: '1', by_domain: @instance.domain), class: 'button' %div{ style: 'float: right' } - - if @domain_block + - if @domain_allow + = link_to t('admin.domain_allows.undo'), admin_domain_allow_path(@domain_allow), class: 'button button--destructive', data: { confirm: t('admin.accounts.are_you_sure'), method: :delete } + - elsif @domain_block = link_to t('admin.domain_blocks.undo'), admin_domain_block_path(@domain_block), class: 'button' - else = link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @instance.domain), class: 'button' diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml index b3bf3849c..1e2ed3f77 100644 --- a/app/views/admin/settings/edit.html.haml +++ b/app/views/admin/settings/edit.html.haml @@ -42,11 +42,12 @@ %hr.spacer/ - .fields-group - = f.input :timeline_preview, as: :boolean, wrapper: :with_label, label: t('admin.settings.timeline_preview.title'), hint: t('admin.settings.timeline_preview.desc_html') + - unless whitelist_mode? + .fields-group + = f.input :timeline_preview, as: :boolean, wrapper: :with_label, label: t('admin.settings.timeline_preview.title'), hint: t('admin.settings.timeline_preview.desc_html') - .fields-group - = f.input :show_known_fediverse_at_about_page, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_known_fediverse_at_about_page.title'), hint: t('admin.settings.show_known_fediverse_at_about_page.desc_html') + .fields-group + = f.input :show_known_fediverse_at_about_page, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_known_fediverse_at_about_page.title'), hint: t('admin.settings.show_known_fediverse_at_about_page.desc_html') .fields-group = f.input :show_staff_badge, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_staff_badge.title'), hint: t('admin.settings.show_staff_badge.desc_html') @@ -54,17 +55,18 @@ .fields-group = f.input :open_deletion, as: :boolean, wrapper: :with_label, label: t('admin.settings.registrations.deletion.title'), hint: t('admin.settings.registrations.deletion.desc_html') - .fields-group - = f.input :activity_api_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.activity_api_enabled.title'), hint: t('admin.settings.activity_api_enabled.desc_html') + - unless whitelist_mode? + .fields-group + = f.input :activity_api_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.activity_api_enabled.title'), hint: t('admin.settings.activity_api_enabled.desc_html') - .fields-group - = f.input :peers_api_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.peers_api_enabled.title'), hint: t('admin.settings.peers_api_enabled.desc_html') + .fields-group + = f.input :peers_api_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.peers_api_enabled.title'), hint: t('admin.settings.peers_api_enabled.desc_html') - .fields-group - = f.input :preview_sensitive_media, as: :boolean, wrapper: :with_label, label: t('admin.settings.preview_sensitive_media.title'), hint: t('admin.settings.preview_sensitive_media.desc_html') + .fields-group + = f.input :preview_sensitive_media, as: :boolean, wrapper: :with_label, label: t('admin.settings.preview_sensitive_media.title'), hint: t('admin.settings.preview_sensitive_media.desc_html') - .fields-group - = f.input :profile_directory, as: :boolean, wrapper: :with_label, label: t('admin.settings.profile_directory.title'), hint: t('admin.settings.profile_directory.desc_html') + .fields-group + = f.input :profile_directory, as: :boolean, wrapper: :with_label, label: t('admin.settings.profile_directory.title'), hint: t('admin.settings.profile_directory.desc_html') .fields-group = f.input :spam_check_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.spam_check_enabled.title'), hint: t('admin.settings.spam_check_enabled.desc_html') @@ -76,7 +78,7 @@ .fields-group = f.input :closed_registrations_message, as: :text, wrapper: :with_block_label, label: t('admin.settings.registrations.closed_message.title'), hint: t('admin.settings.registrations.closed_message.desc_html'), input_html: { rows: 8 } - = f.input :site_extended_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description_extended.title'), hint: t('admin.settings.site_description_extended.desc_html'), input_html: { rows: 8 } + = f.input :site_extended_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description_extended.title'), hint: t('admin.settings.site_description_extended.desc_html'), input_html: { rows: 8 } unless whitelist_mode? = f.input :site_terms, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_terms.title'), hint: t('admin.settings.site_terms.desc_html'), input_html: { rows: 8 } = f.input :custom_css, wrapper: :with_block_label, as: :text, input_html: { rows: 8 }, label: t('admin.settings.custom_css.title'), hint: t('admin.settings.custom_css.desc_html') diff --git a/app/views/auth/registrations/new.html.haml b/app/views/auth/registrations/new.html.haml index b4a7cced5..83384d737 100644 --- a/app/views/auth/registrations/new.html.haml +++ b/app/views/auth/registrations/new.html.haml @@ -33,7 +33,7 @@ = f.input :invite_code, as: :hidden .fields-group - = f.input :agreement, as: :boolean, wrapper: :with_label, label: t('auth.checkbox_agreement_html', rules_path: about_more_path, terms_path: terms_path) + = f.input :agreement, as: :boolean, wrapper: :with_label, label: whitelist_mode? ? t('auth.checkbox_agreement_without_rules_html', terms_path: terms_path) : t('auth.checkbox_agreement_html', rules_path: about_more_path, terms_path: terms_path) .actions = f.button :button, @invite.present? ? t('auth.register') : sign_up_message, type: :submit diff --git a/app/views/layouts/public.html.haml b/app/views/layouts/public.html.haml index 2929ac599..69738a2f7 100644 --- a/app/views/layouts/public.html.haml +++ b/app/views/layouts/public.html.haml @@ -10,10 +10,13 @@ = link_to root_url, class: 'brand' do = svg_logo_full - = link_to t('directories.directory'), explore_path, class: 'nav-link optional' if Setting.profile_directory - = link_to t('about.about_this'), about_more_path, class: 'nav-link optional' - = link_to t('about.apps'), 'https://joinmastodon.org/apps', class: 'nav-link optional' + - unless whitelist_mode? + = link_to t('directories.directory'), explore_path, class: 'nav-link optional' if Setting.profile_directory + = link_to t('about.about_this'), about_more_path, class: 'nav-link optional' + = link_to t('about.apps'), 'https://joinmastodon.org/apps', class: 'nav-link optional' + .nav-center + .nav-right - if user_signed_in? = link_to t('settings.back'), root_url, class: 'nav-link nav-button webapp-btn' diff --git a/config/initializers/2_whitelist_mode.rb b/config/initializers/2_whitelist_mode.rb new file mode 100644 index 000000000..a17ad07a2 --- /dev/null +++ b/config/initializers/2_whitelist_mode.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +Rails.application.configure do + config.x.whitelist_mode = ENV['WHITELIST_MODE'] == 'true' +end diff --git a/config/locales/en.yml b/config/locales/en.yml index 9e1be87be..6c1a34300 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -186,6 +186,7 @@ en: username: Username warn: Warn web: Web + whitelisted: Whitelisted action_logs: actions: assigned_to_self_report: "%{name} assigned report %{target} to themselves" @@ -269,6 +270,11 @@ en: week_interactions: interactions this week week_users_active: active this week week_users_new: users this week + domain_allows: + add_new: Whitelist domain + created_msg: Domain has been successfully whitelisted + destroyed_msg: Domain has been removed from the whitelist + undo: Remove from whitelist domain_blocks: add_new: Add new domain block created_msg: Domain block is now being processed @@ -524,6 +530,7 @@ en: apply_for_account: Request an invite change_password: Password checkbox_agreement_html: I agree to the server rules and terms of service + checkbox_agreement_without_rules_html: I agree to the terms of service delete_account: Delete account delete_account_html: If you wish to delete your account, you can proceed here. You will be asked for confirmation. didnt_get_confirmation: Didn't receive confirmation instructions? diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 12a7ec2b3..10b30e627 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -38,6 +38,8 @@ en: setting_use_pending_items: Hide timeline updates behind a click instead of automatically scrolling the feed username: Your username will be unique on %{domain} whole_word: When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word + domain_allow: + domain: This domain will be able to fetch data from this server and incoming data from it will be processed and stored featured_tag: name: 'You might want to use one of these:' imports: diff --git a/config/navigation.rb b/config/navigation.rb index 5ab2e4399..9b46da603 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -39,7 +39,7 @@ SimpleNavigation::Configuration.run do |navigation| s.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_url, highlights_on: %r{/admin/accounts|/admin/pending_accounts} s.item :invites, safe_join([fa_icon('user-plus fw'), t('admin.invites.title')]), admin_invites_path s.item :tags, safe_join([fa_icon('tag fw'), t('admin.tags.title')]), admin_tags_path - s.item :instances, safe_join([fa_icon('cloud fw'), t('admin.instances.title')]), admin_instances_url(limited: '1'), highlights_on: %r{/admin/instances|/admin/domain_blocks}, if: -> { current_user.admin? } + s.item :instances, safe_join([fa_icon('cloud fw'), t('admin.instances.title')]), admin_instances_url(limited: whitelist_mode? ? nil : '1'), highlights_on: %r{/admin/instances|/admin/domain_blocks|/admin/domain_allows}, if: -> { current_user.admin? } s.item :email_domain_blocks, safe_join([fa_icon('envelope fw'), t('admin.email_domain_blocks.title')]), admin_email_domain_blocks_url, highlights_on: %r{/admin/email_domain_blocks}, if: -> { current_user.admin? } end diff --git a/config/routes.rb b/config/routes.rb index b6c215888..04424bbbd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -154,6 +154,7 @@ Rails.application.routes.draw do namespace :admin do get '/dashboard', to: 'dashboard#index' + resources :domain_allows, only: [:new, :create, :show, :destroy] resources :domain_blocks, only: [:new, :create, :show, :destroy] resources :email_domain_blocks, only: [:index, :new, :create, :destroy] resources :action_logs, only: [:index] diff --git a/db/migrate/20190705002136_create_domain_allows.rb b/db/migrate/20190705002136_create_domain_allows.rb new file mode 100644 index 000000000..83b0728d9 --- /dev/null +++ b/db/migrate/20190705002136_create_domain_allows.rb @@ -0,0 +1,9 @@ +class CreateDomainAllows < ActiveRecord::Migration[5.2] + def change + create_table :domain_allows do |t| + t.string :domain, default: '', null: false, index: { unique: true } + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 1847305c7..2d83d8b76 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_07_26_175042) do +ActiveRecord::Schema.define(version: 2019_07_28_084117) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -245,6 +245,13 @@ ActiveRecord::Schema.define(version: 2019_07_26_175042) do t.index ["account_id"], name: "index_custom_filters_on_account_id" end + create_table "domain_allows", force: :cascade do |t| + t.string "domain", default: "", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["domain"], name: "index_domain_allows_on_domain", unique: true + end + create_table "domain_blocks", force: :cascade do |t| t.string "domain", default: "", null: false t.datetime "created_at", null: false diff --git a/lib/mastodon/domains_cli.rb b/lib/mastodon/domains_cli.rb index b081581fe..f30062363 100644 --- a/lib/mastodon/domains_cli.rb +++ b/lib/mastodon/domains_cli.rb @@ -12,17 +12,33 @@ module Mastodon end option :dry_run, type: :boolean - desc 'purge DOMAIN', 'Remove accounts from a DOMAIN without a trace' + option :whitelist_mode, type: :boolean + desc 'purge [DOMAIN]', 'Remove accounts from a DOMAIN without a trace' long_desc <<-LONG_DESC Remove all accounts from a given DOMAIN without leaving behind any records. Unlike a suspension, if the DOMAIN still exists in the wild, it means the accounts could return if they are resolved again. + + When the --whitelist-mode option is given, instead of purging accounts + from a single domain, all accounts from domains that are not whitelisted + are removed from the database. LONG_DESC - def purge(domain) + def purge(domain = nil) removed = 0 dry_run = options[:dry_run] ? ' (DRY RUN)' : '' - Account.where(domain: domain).find_each do |account| + scope = begin + if options[:whitelist_mode] + Account.remote.where.not(domain: DomainAllow.pluck(:domain)) + elsif domain.present? + Account.remote.where(domain: domain) + else + say('No domain given', :red) + exit(1) + end + end + + scope.find_each do |account| SuspendAccountService.new.call(account, destroy: true) unless options[:dry_run] removed += 1 say('.', :green, false) diff --git a/spec/fabricators/domain_allow_fabricator.rb b/spec/fabricators/domain_allow_fabricator.rb new file mode 100644 index 000000000..6226b1e20 --- /dev/null +++ b/spec/fabricators/domain_allow_fabricator.rb @@ -0,0 +1,3 @@ +Fabricator(:domain_allow) do + domain "MyString" +end diff --git a/spec/models/domain_allow_spec.rb b/spec/models/domain_allow_spec.rb new file mode 100644 index 000000000..e65435127 --- /dev/null +++ b/spec/models/domain_allow_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe DomainAllow, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/streaming/index.js b/streaming/index.js index 0529804b1..304e7e046 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -12,6 +12,7 @@ const uuid = require('uuid'); const fs = require('fs'); const env = process.env.NODE_ENV || 'development'; +const alwaysRequireAuth = process.env.WHITELIST_MODE === 'true' || process.env.AUTHORIZED_FETCH === 'true'; dotenv.config({ path: env === 'production' ? '.env.production' : '.env', @@ -271,7 +272,7 @@ const startWorker = (workerId) => { const wsVerifyClient = (info, cb) => { const location = url.parse(info.req.url, true); - const authRequired = !PUBLIC_STREAMS.some(stream => stream === location.query.stream); + const authRequired = alwaysRequireAuth || !PUBLIC_STREAMS.some(stream => stream === location.query.stream); const allowedScopes = []; if (authRequired) { @@ -306,7 +307,7 @@ const startWorker = (workerId) => { return; } - const authRequired = !PUBLIC_ENDPOINTS.some(endpoint => endpoint === req.path); + const authRequired = alwaysRequireAuth || !PUBLIC_ENDPOINTS.some(endpoint => endpoint === req.path); const allowedScopes = []; if (authRequired) { -- cgit From 115dab78f1cc5357281dcb593f04ac8b2629cec6 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 5 Aug 2019 19:54:29 +0200 Subject: Change admin UI for hashtags and add back whitelisted trends (#11490) Fix #271 Add back the `GET /api/v1/trends` API with the caveat that it does not return tags that have not been allowed to trend by the staff. When a hashtag begins to trend (internally) and that hashtag has not been previously reviewed by the staff, the staff is notified. The new admin UI for hashtags allows filtering hashtags by where they are used (e.g. in the profile directory), whether they have been reviewed or are pending reviewal, they show by how many people the hashtag is used in the directory, how many people used it today, how many statuses with it have been created today, and it allows fixing the name of the hashtag to make it more readable. The disallowed hashtags feature has been reworked. It is now controlled from the admin UI for hashtags instead of from the file `config/settings.yml` --- app/controllers/admin/dashboard_controller.rb | 2 +- app/controllers/admin/tags_controller.rb | 36 ++++++++----- app/controllers/api/v1/trends_controller.rb | 17 ++++++ app/controllers/settings/preferences_controller.rb | 2 +- app/helpers/admin/filter_helper.rb | 5 +- app/mailers/admin_mailer.rb | 10 ++++ app/models/application_record.rb | 11 ++++ app/models/tag.rb | 60 +++++++++++++++++++--- app/models/trending_tags.rb | 48 ++++++++--------- app/models/user.rb | 4 ++ app/policies/tag_policy.rb | 4 +- app/validators/disallowed_hashtags_validator.rb | 21 +------- app/views/admin/dashboard/index.html.haml | 2 +- app/views/admin/tags/_tag.html.haml | 24 +++++---- app/views/admin/tags/index.html.haml | 26 +++++----- app/views/admin/tags/show.html.haml | 16 ++++++ app/views/admin_mailer/new_trending_tag.text.erb | 5 ++ .../preferences/notifications/show.html.haml | 1 + config/locales/en.yml | 18 ++++--- config/locales/simple_form.en.yml | 7 +++ config/navigation.rb | 2 +- config/routes.rb | 9 +--- config/settings.yml | 1 + .../20190805123746_add_capabilities_to_tags.rb | 9 ++++ db/schema.rb | 7 ++- spec/controllers/admin/tags_controller_spec.rb | 56 ++------------------ spec/policies/tag_policy_spec.rb | 2 +- .../disallowed_hashtags_validator_spec.rb | 26 +++++----- 28 files changed, 258 insertions(+), 173 deletions(-) create mode 100644 app/controllers/api/v1/trends_controller.rb create mode 100644 app/views/admin/tags/show.html.haml create mode 100644 app/views/admin_mailer/new_trending_tag.text.erb create mode 100644 db/migrate/20190805123746_add_capabilities_to_tags.rb (limited to 'app/helpers') diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index e74e4755f..70afdedd7 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -27,7 +27,7 @@ module Admin @saml_enabled = ENV['SAML_ENABLED'] == 'true' @pam_enabled = ENV['PAM_ENABLED'] == 'true' @hidden_service = ENV['ALLOW_ACCESS_TO_HIDDEN_SERVICE'] == 'true' - @trending_hashtags = TrendingTags.get(7) + @trending_hashtags = TrendingTags.get(10, filtered: false) @profile_directory = Setting.profile_directory @timeline_preview = Setting.timeline_preview @spam_check_enabled = Setting.spam_check_enabled diff --git a/app/controllers/admin/tags_controller.rb b/app/controllers/admin/tags_controller.rb index e9f4f2cfa..0e9dda302 100644 --- a/app/controllers/admin/tags_controller.rb +++ b/app/controllers/admin/tags_controller.rb @@ -4,41 +4,49 @@ module Admin class TagsController < BaseController before_action :set_tags, only: :index before_action :set_tag, except: :index - before_action :set_filter_params def index authorize :tag, :index? end - def hide - authorize @tag, :hide? - @tag.account_tag_stat.update!(hidden: true) - redirect_to admin_tags_path(@filter_params) + def show + authorize @tag, :show? end - def unhide - authorize @tag, :unhide? - @tag.account_tag_stat.update!(hidden: false) - redirect_to admin_tags_path(@filter_params) + def update + authorize @tag, :update? + + if @tag.update(tag_params.merge(reviewed_at: Time.now.utc)) + redirect_to admin_tag_path(@tag.id) + else + render :show + end end private def set_tags - @tags = Tag.discoverable - @tags.merge!(Tag.hidden) if filter_params[:hidden] + @tags = filtered_tags.page(params[:page]) end def set_tag @tag = Tag.find(params[:id]) end - def set_filter_params - @filter_params = filter_params.to_hash.symbolize_keys + def filtered_tags + scope = Tag + scope = scope.discoverable if filter_params[:context] == 'directory' + scope = scope.reviewed if filter_params[:review] == 'reviewed' + scope = scope.pending_review if filter_params[:review] == 'pending_review' + scope.reorder(score: :desc) end def filter_params - params.permit(:hidden) + params.slice(:context, :review).permit(:context, :review) + end + + def tag_params + params.require(:tag).permit(:name, :trendable, :usable, :listable) end end end diff --git a/app/controllers/api/v1/trends_controller.rb b/app/controllers/api/v1/trends_controller.rb new file mode 100644 index 000000000..bcea9857e --- /dev/null +++ b/app/controllers/api/v1/trends_controller.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class Api::V1::TrendsController < Api::BaseController + before_action :set_tags + + respond_to :json + + def index + render json: @tags, each_serializer: REST::TagSerializer + end + + private + + def set_tags + @tags = TrendingTags.get(limit_param(10)) + end +end diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 742c97cdb..d548072a8 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -56,7 +56,7 @@ class Settings::PreferencesController < Settings::BaseController :setting_advanced_layout, :setting_use_blurhash, :setting_use_pending_items, - notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account), + notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag), interactions: %i(must_be_follower must_be_following must_be_following_dm) ) end diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb index 0bda25974..506429e10 100644 --- a/app/helpers/admin/filter_helper.rb +++ b/app/helpers/admin/filter_helper.rb @@ -5,15 +5,16 @@ 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(hidden).freeze + TAGS_FILTERS = %i(context review).freeze INSTANCES_FILTERS = %i(limited by_domain).freeze FOLLOWERS_FILTERS = %i(relationship status by_domain activity order).freeze FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS + TAGS_FILTERS + INSTANCES_FILTERS + FOLLOWERS_FILTERS def filter_link_to(text, link_to_params, link_class_params = link_to_params) - new_url = filtered_url_for(link_to_params) + new_url = filtered_url_for(link_to_params) new_class = filtered_url_for(link_class_params) + link_to text, new_url, class: filter_link_class(new_class) end diff --git a/app/mailers/admin_mailer.rb b/app/mailers/admin_mailer.rb index 9ab3e2bbd..8abce5f05 100644 --- a/app/mailers/admin_mailer.rb +++ b/app/mailers/admin_mailer.rb @@ -24,4 +24,14 @@ class AdminMailer < ApplicationMailer mail to: @me.user_email, subject: I18n.t('admin_mailer.new_pending_account.subject', instance: @instance, username: @account.username) end end + + def new_trending_tag(recipient, tag) + @tag = tag + @me = recipient + @instance = Rails.configuration.x.local_domain + + locale_for_account(@me) do + mail to: @me.user_email, subject: I18n.t('admin_mailer.new_trending_tag.subject', instance: @instance, name: @tag.name) + end + end end diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 83134d41a..c1b873da6 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -2,5 +2,16 @@ class ApplicationRecord < ActiveRecord::Base self.abstract_class = true + include Remotable + + def boolean_with_default(key, default_value) + value = attributes[key] + + if value.nil? + default_value + else + value + end + end end diff --git a/app/models/tag.rb b/app/models/tag.rb index c7f0af86d..6a02581fa 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -3,11 +3,16 @@ # # Table name: tags # -# id :bigint(8) not null, primary key -# name :string default(""), not null -# created_at :datetime not null -# updated_at :datetime not null -# score :integer +# id :bigint(8) not null, primary key +# name :string default(""), not null +# created_at :datetime not null +# updated_at :datetime not null +# score :integer +# usable :boolean +# trendable :boolean +# listable :boolean +# reviewed_at :datetime +# requested_review_at :datetime # class Tag < ApplicationRecord @@ -22,16 +27,17 @@ class Tag < ApplicationRecord HASHTAG_RE = /(?:^|[^\/\)\w])#(#{HASHTAG_NAME_RE})/i validates :name, presence: true, format: { with: /\A(#{HASHTAG_NAME_RE})\z/i } + validate :validate_name_change, if: -> { !new_record? && name_changed? } - scope :discoverable, -> { joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).where(account_tag_stats: { hidden: false }).order(Arel.sql('account_tag_stats.accounts_count desc')) } - scope :hidden, -> { where(account_tag_stats: { hidden: true }) } + scope :reviewed, -> { where.not(reviewed_at: nil) } + scope :pending_review, -> { where(reviewed_at: nil).where.not(requested_review_at: nil) } + scope :discoverable, -> { where.not(listable: false).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')) } delegate :accounts_count, :accounts_count=, :increment_count!, :decrement_count!, - :hidden?, to: :account_tag_stat after_save :save_account_tag_stat @@ -48,6 +54,40 @@ class Tag < ApplicationRecord name end + def usable + boolean_with_default('usable', true) + end + + alias usable? usable + + def listable + boolean_with_default('listable', true) + end + + alias listable? listable + + def trendable + boolean_with_default('trendable', false) + end + + alias trendable? trendable + + def requires_review? + reviewed_at.nil? + end + + def reviewed? + reviewed_at.present? + end + + def requested_review? + requested_review_at.present? + end + + def trending? + TrendingTags.trending?(self) + end + def history days = [] @@ -117,4 +157,8 @@ class Tag < ApplicationRecord return unless account_tag_stat&.changed? account_tag_stat.save end + + def validate_name_change + errors.add(:name, I18n.t('tags.does_not_match_previous_name')) unless name_was.mb_chars.casecmp(name.mb_chars).zero? + end end diff --git a/app/models/trending_tags.rb b/app/models/trending_tags.rb index 211c8f1dc..e9b9b25e3 100644 --- a/app/models/trending_tags.rb +++ b/app/models/trending_tags.rb @@ -10,20 +10,28 @@ class TrendingTags include Redisable def record_use!(tag, account, at_time = Time.now.utc) - return if disallowed_hashtags.include?(tag.name) || account.silenced? || account.bot? + return if account.silenced? || account.bot? || !tag.usable? || !(tag.trendable? || tag.requires_review?) increment_historical_use!(tag.id, at_time) increment_unique_use!(tag.id, account.id, at_time) - increment_vote!(tag.id, at_time) + increment_vote!(tag, at_time) end - def get(limit) - key = "#{KEY}:#{Time.now.utc.beginning_of_day.to_i}" - tag_ids = redis.zrevrange(key, 0, limit - 1).map(&:to_i) - tags = Tag.where(id: tag_ids).to_a.each_with_object({}) { |tag, h| h[tag.id] = tag } + def get(limit, filtered: true) + tag_ids = redis.zrevrange("#{KEY}:#{Time.now.utc.beginning_of_day.to_i}", 0, limit - 1).map(&:to_i) + + tags = Tag.where(id: tag_ids) + tags = tags.where(trendable: true) if filtered + tags = tags.each_with_object({}) { |tag, h| h[tag.id] = tag } + tag_ids.map { |tag_id| tags[tag_id] }.compact end + def trending?(tag) + rank = redis.zrevrank("#{KEY}:#{Time.now.utc.beginning_of_day.to_i}", tag.id) + rank.present? && rank <= 10 + end + private def increment_historical_use!(tag_id, at_time) @@ -38,33 +46,27 @@ class TrendingTags redis.expire(key, EXPIRE_HISTORY_AFTER) end - def increment_vote!(tag_id, at_time) + def increment_vote!(tag, at_time) key = "#{KEY}:#{at_time.beginning_of_day.to_i}" - expected = redis.pfcount("activity:tags:#{tag_id}:#{(at_time - 1.day).beginning_of_day.to_i}:accounts").to_f + expected = redis.pfcount("activity:tags:#{tag.id}:#{(at_time - 1.day).beginning_of_day.to_i}:accounts").to_f expected = 1.0 if expected.zero? - observed = redis.pfcount("activity:tags:#{tag_id}:#{at_time.beginning_of_day.to_i}:accounts").to_f + observed = redis.pfcount("activity:tags:#{tag.id}:#{at_time.beginning_of_day.to_i}:accounts").to_f if expected > observed || observed < THRESHOLD - redis.zrem(key, tag_id.to_s) + redis.zrem(key, tag.id) else - score = ((observed - expected)**2) / expected - added = redis.zadd(key, score, tag_id.to_s) - bump_tag_score!(tag_id) if added + score = ((observed - expected)**2) / expected + old_rank = redis.zrevrank(key, tag.id) + + redis.zadd(key, score, tag.id) + request_review!(tag) if (old_rank.nil? || old_rank > 10) && redis.zrevrank(key, tag.id) <= 10 && !tag.trendable? && tag.requires_review? && !tag.requested_review? end redis.expire(key, EXPIRE_TRENDS_AFTER) end - def bump_tag_score!(tag_id) - Tag.where(id: tag_id).update_all('score = COALESCE(score, 0) + 1') - end - - def disallowed_hashtags - return @disallowed_hashtags if defined?(@disallowed_hashtags) - - @disallowed_hashtags = Setting.disallowed_hashtags.nil? ? [] : Setting.disallowed_hashtags - @disallowed_hashtags = @disallowed_hashtags.split(' ') if @disallowed_hashtags.is_a? String - @disallowed_hashtags = @disallowed_hashtags.map(&:downcase) + def request_review!(tag) + User.staff.includes(:account).find_each { |u| AdminMailer.new_trending_tag(u.account, tag).deliver_later! if u.allows_trending_tag_emails? } end end end diff --git a/app/models/user.rb b/app/models/user.rb index 6806c0362..b83e26af3 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -207,6 +207,10 @@ class User < ApplicationRecord settings.notification_emails['pending_account'] end + def allows_trending_tag_emails? + settings.notification_emails['trending_tag'] + end + def hides_network? @hides_network ||= settings.hide_network end diff --git a/app/policies/tag_policy.rb b/app/policies/tag_policy.rb index c63de01db..aaf70fcab 100644 --- a/app/policies/tag_policy.rb +++ b/app/policies/tag_policy.rb @@ -5,11 +5,11 @@ class TagPolicy < ApplicationPolicy staff? end - def hide? + def show? staff? end - def unhide? + def update? staff? end end diff --git a/app/validators/disallowed_hashtags_validator.rb b/app/validators/disallowed_hashtags_validator.rb index ee06b20f6..d745b767f 100644 --- a/app/validators/disallowed_hashtags_validator.rb +++ b/app/validators/disallowed_hashtags_validator.rb @@ -4,24 +4,7 @@ class DisallowedHashtagsValidator < ActiveModel::Validator def validate(status) return unless status.local? && !status.reblog? - @status = status - tags = select_tags - - status.errors.add(:text, I18n.t('statuses.disallowed_hashtags', tags: tags.join(', '), count: tags.size)) unless tags.empty? - end - - private - - def select_tags - tags = Extractor.extract_hashtags(@status.text) - tags.keep_if { |tag| disallowed_hashtags.include? tag.downcase } - end - - def disallowed_hashtags - return @disallowed_hashtags if @disallowed_hashtags - - @disallowed_hashtags = Setting.disallowed_hashtags.nil? ? [] : Setting.disallowed_hashtags - @disallowed_hashtags = @disallowed_hashtags.split(' ') if @disallowed_hashtags.is_a? String - @disallowed_hashtags = @disallowed_hashtags.map(&:downcase) + disallowed_hashtags = Tag.matching_name(Extractor.extract_hashtags(status.text)).reject(&:usable?) + status.errors.add(:text, I18n.t('statuses.disallowed_hashtags', tags: disallowed_hashtags.map(&:name).join(', '), count: disallowed_hashtags.size)) unless disallowed_hashtags.empty? end end diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 77cc1a2a0..910896075 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -107,5 +107,5 @@ %ul - @trending_hashtags.each do |tag| %li - = link_to "##{tag.name}", web_url("timelines/tag/#{tag.name}") + = link_to content_tag(:span, "##{tag.name}", class: !tag.trendable? && !tag.reviewed? ? 'warning-hint' : (!tag.trendable? ? 'negative-hint' : nil)), admin_tag_path(tag.id) %span.pull-right= number_with_delimiter(tag.history[0][:accounts].to_i) diff --git a/app/views/admin/tags/_tag.html.haml b/app/views/admin/tags/_tag.html.haml index 961b83f93..91af8e492 100644 --- a/app/views/admin/tags/_tag.html.haml +++ b/app/views/admin/tags/_tag.html.haml @@ -1,12 +1,16 @@ -%tr - %td - = link_to explore_hashtag_path(tag) do +.directory__tag + = link_to admin_tag_path(tag.id) do + %h4 = fa_icon 'hashtag' = tag.name - %td - = t('directories.people', count: tag.accounts_count) - %td - - if tag.hidden? - = table_link_to 'eye', t('admin.tags.unhide'), unhide_admin_tag_path(tag.id, **@filter_params), method: :post - - else - = table_link_to 'eye-slash', t('admin.tags.hide'), hide_admin_tag_path(tag.id, **@filter_params), method: :post + + %small + = t('admin.tags.in_directory', count: tag.accounts_count) + • + = t('admin.tags.unique_uses_today', count: tag.history.first[:accounts]) + + - if tag.trending? + = fa_icon 'fire fw' + = t('admin.tags.trending_right_now') + + .trends__item__current= number_to_human tag.history.first[:uses], strip_insignificant_zeros: true diff --git a/app/views/admin/tags/index.html.haml b/app/views/admin/tags/index.html.haml index 4ba395860..5e4ee21f5 100644 --- a/app/views/admin/tags/index.html.haml +++ b/app/views/admin/tags/index.html.haml @@ -3,17 +3,19 @@ .filters .filter-subset - %strong= t('admin.reports.status') + %strong= t('admin.tags.context') %ul - %li= filter_link_to t('admin.tags.visible'), hidden: nil - %li= filter_link_to t('admin.tags.hidden'), hidden: '1' + %li= filter_link_to t('generic.all'), context: nil + %li= filter_link_to t('admin.tags.directory'), context: 'directory' -.table-wrapper - %table.table - %thead - %tr - %th= t('admin.tags.name') - %th= t('admin.tags.accounts') - %th - %tbody - = render @tags + .filter-subset + %strong= t('admin.tags.review') + %ul + %li= filter_link_to t('generic.all'), review: nil + %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' + +%hr.spacer/ + += render @tags += paginate @tags diff --git a/app/views/admin/tags/show.html.haml b/app/views/admin/tags/show.html.haml new file mode 100644 index 000000000..27c8dc92b --- /dev/null +++ b/app/views/admin/tags/show.html.haml @@ -0,0 +1,16 @@ +- content_for :page_title do + = "##{@tag.name}" + += simple_form_for @tag, url: admin_tag_path(@tag.id) do |f| + = render 'shared/error_messages', object: @tag + + .fields-group + = f.input :name, wrapper: :with_block_label + + .fields-group + = f.input :usable, as: :boolean, wrapper: :with_label + = f.input :trendable, as: :boolean, wrapper: :with_label + = f.input :listable, as: :boolean, wrapper: :with_label + + .actions + = f.button :button, t('generic.save_changes'), type: :submit diff --git a/app/views/admin_mailer/new_trending_tag.text.erb b/app/views/admin_mailer/new_trending_tag.text.erb new file mode 100644 index 000000000..f3087df37 --- /dev/null +++ b/app/views/admin_mailer/new_trending_tag.text.erb @@ -0,0 +1,5 @@ +<%= raw t('application_mailer.salutation', name: display_name(@me)) %> + +<%= raw t('admin_mailer.new_trending_tag.body', name: @tag.name) %> + +<%= raw t('application_mailer.view')%> <%= admin_tags_url(review: 'pending_review') %> diff --git a/app/views/settings/preferences/notifications/show.html.haml b/app/views/settings/preferences/notifications/show.html.haml index acc646fc3..f666ae4ff 100644 --- a/app/views/settings/preferences/notifications/show.html.haml +++ b/app/views/settings/preferences/notifications/show.html.haml @@ -15,6 +15,7 @@ - if current_user.staff? = ff.input :report, as: :boolean, wrapper: :with_label = ff.input :pending_account, as: :boolean, wrapper: :with_label + = ff.input :trending_tag, as: :boolean, wrapper: :with_label .fields-group = f.simple_fields_for :notification_emails, hash_to_object(current_user.settings.notification_emails) do |ff| diff --git a/config/locales/en.yml b/config/locales/en.yml index 6c1a34300..9b62aac3a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -483,13 +483,14 @@ en: title: Account statuses with_media: With media tags: - accounts: Accounts - hidden: Hidden - hide: Hide from directory - name: Hashtag + context: Context + directory: In directory + in_directory: "%{count} in directory" + review: Review status + reviewed: Reviewed title: Hashtags - unhide: Show in directory - visible: Visible + trending_right_now: Trending right now + unique_uses_today: "%{count} posting today" title: Administration warning_presets: add_new: Add new @@ -505,6 +506,9 @@ en: body: "%{reporter} has reported %{target}" body_remote: Someone from %{domain} has reported %{target} subject: New report for %{instance} (#%{id}) + new_trending_tag: + body: 'The hashtag #%{name} is trending today, but has not been previously reviewed. It will not be displayed publicly unless you allow it to, or just save the form as it is to never hear about it again.' + subject: New hashtag up for review on %{instance} (#%{name}) appearance: advanced_web_interface: Advanced web interface advanced_web_interface_hint: 'If you want to make use of your entire screen width, the advanced web interface allows you to configure many different columns to see as much information at the same time as you want: Home, notifications, federated timeline, any number of lists and hashtags.' @@ -939,6 +943,8 @@ en: pinned: Pinned toot reblogged: boosted sensitive_content: Sensitive content + tags: + does_not_match_previous_name: does not match the previous name terms: body_html: |

Privacy Policy

diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 10b30e627..6fdfc9d7b 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -48,6 +48,8 @@ en: text: This will help us review your application sessions: otp: 'Enter the two-factor code generated by your phone app or use one of your recovery codes:' + tag: + name: You can only change the casing of the letters, for example, to make it more readable user: chosen_languages: When checked, only toots in selected languages will be displayed in public timelines labels: @@ -137,6 +139,11 @@ en: pending_account: Send e-mail when a new account needs review reblog: Send e-mail when someone boosts your status report: Send e-mail when a new report is submitted + trending_tag: Send e-mail when an unreviewed hashtag is trending + tag: + listable: Allow this hashtag to appear on the profile directory + trendable: Allow this hashtag to appear under trends + usable: Allow toots to use this hashtag 'no': 'No' recommended: Recommended required: diff --git a/config/navigation.rb b/config/navigation.rb index 9b46da603..38668bbf7 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -38,7 +38,7 @@ SimpleNavigation::Configuration.run do |navigation| s.item :reports, safe_join([fa_icon('flag fw'), t('admin.reports.title')]), admin_reports_url, highlights_on: %r{/admin/reports} s.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_url, highlights_on: %r{/admin/accounts|/admin/pending_accounts} s.item :invites, safe_join([fa_icon('user-plus fw'), t('admin.invites.title')]), admin_invites_path - s.item :tags, safe_join([fa_icon('tag fw'), t('admin.tags.title')]), admin_tags_path + s.item :tags, safe_join([fa_icon('hashtag fw'), t('admin.tags.title')]), admin_tags_path, highlights_on: %r{/admin/tags} s.item :instances, safe_join([fa_icon('cloud fw'), t('admin.instances.title')]), admin_instances_url(limited: whitelist_mode? ? nil : '1'), highlights_on: %r{/admin/instances|/admin/domain_blocks|/admin/domain_allows}, if: -> { current_user.admin? } s.item :email_domain_blocks, safe_join([fa_icon('envelope fw'), t('admin.email_domain_blocks.title')]), admin_email_domain_blocks_url, highlights_on: %r{/admin/email_domain_blocks}, if: -> { current_user.admin? } end diff --git a/config/routes.rb b/config/routes.rb index 04424bbbd..60f7d2e05 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -243,13 +243,7 @@ Rails.application.routes.draw do end resources :account_moderation_notes, only: [:create, :destroy] - - resources :tags, only: [:index] do - member do - post :hide - post :unhide - end - end + resources :tags, only: [:index, :show, :update] end get '/admin', to: redirect('/admin/dashboard', status: 302) @@ -311,6 +305,7 @@ Rails.application.routes.draw do resources :mutes, only: [:index] resources :favourites, only: [:index] resources :reports, only: [:create] + resources :trends, only: [:index] resources :filters, only: [:index, :create, :show, :update, :destroy] resources :endorsements, only: [:index] diff --git a/config/settings.yml b/config/settings.yml index ad2970bb7..10180201f 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -43,6 +43,7 @@ defaults: &defaults digest: true report: true pending_account: true + trending_tag: true interactions: must_be_follower: false must_be_following: false diff --git a/db/migrate/20190805123746_add_capabilities_to_tags.rb b/db/migrate/20190805123746_add_capabilities_to_tags.rb new file mode 100644 index 000000000..43c7763b1 --- /dev/null +++ b/db/migrate/20190805123746_add_capabilities_to_tags.rb @@ -0,0 +1,9 @@ +class AddCapabilitiesToTags < ActiveRecord::Migration[5.2] + def change + add_column :tags, :usable, :boolean + add_column :tags, :trendable, :boolean + add_column :tags, :listable, :boolean + add_column :tags, :reviewed_at, :datetime + add_column :tags, :requested_review_at, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index e3af9c31a..d1b6825b4 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_07_29_185330) do +ActiveRecord::Schema.define(version: 2019_08_05_123746) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -660,6 +660,11 @@ ActiveRecord::Schema.define(version: 2019_07_29_185330) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "score" + t.boolean "usable" + t.boolean "trendable" + t.boolean "listable" + t.datetime "reviewed_at" + t.datetime "requested_review_at" t.index "lower((name)::text)", name: "index_tags_on_name_lower", unique: true end diff --git a/spec/controllers/admin/tags_controller_spec.rb b/spec/controllers/admin/tags_controller_spec.rb index 3af994071..5c1944fc7 100644 --- a/spec/controllers/admin/tags_controller_spec.rb +++ b/spec/controllers/admin/tags_controller_spec.rb @@ -10,62 +10,14 @@ RSpec.describe Admin::TagsController, type: :controller do end describe 'GET #index' do - before do - account_tag_stat = Fabricate(:tag).account_tag_stat - account_tag_stat.update(hidden: hidden, accounts_count: 1) - get :index, params: { hidden: hidden } - end - - context 'with hidden tags' do - let(:hidden) { true } - - it 'returns status 200' do - expect(response).to have_http_status(200) - end - end - - context 'without hidden tags' do - let(:hidden) { false } - - it 'returns status 200' do - expect(response).to have_http_status(200) - end - end - end - - describe 'POST #hide' do - let(:tag) { Fabricate(:tag) } + let!(:tag) { Fabricate(:tag) } before do - tag.account_tag_stat.update(hidden: false) - post :hide, params: { id: tag.id } - end - - it 'hides tag' do - tag.reload - expect(tag).to be_hidden - end - - it 'redirects to admin_tags_path' do - expect(response).to redirect_to(admin_tags_path(controller.instance_variable_get(:@filter_params))) - end - end - - describe 'POST #unhide' do - let(:tag) { Fabricate(:tag) } - - before do - tag.account_tag_stat.update(hidden: true) - post :unhide, params: { id: tag.id } - end - - it 'unhides tag' do - tag.reload - expect(tag).not_to be_hidden + get :index end - it 'redirects to admin_tags_path' do - expect(response).to redirect_to(admin_tags_path(controller.instance_variable_get(:@filter_params))) + it 'returns status 200' do + expect(response).to have_http_status(200) end end end diff --git a/spec/policies/tag_policy_spec.rb b/spec/policies/tag_policy_spec.rb index c7afaa7c9..c63875dc0 100644 --- a/spec/policies/tag_policy_spec.rb +++ b/spec/policies/tag_policy_spec.rb @@ -8,7 +8,7 @@ RSpec.describe TagPolicy do let(:admin) { Fabricate(:user, admin: true).account } let(:john) { Fabricate(:user).account } - permissions :index?, :hide?, :unhide? do + permissions :index?, :show?, :update? do context 'staff?' do it 'permits' do expect(subject).to permit(admin, Tag) diff --git a/spec/validators/disallowed_hashtags_validator_spec.rb b/spec/validators/disallowed_hashtags_validator_spec.rb index 8ec1302ab..9deec0bb9 100644 --- a/spec/validators/disallowed_hashtags_validator_spec.rb +++ b/spec/validators/disallowed_hashtags_validator_spec.rb @@ -3,42 +3,44 @@ require 'rails_helper' RSpec.describe DisallowedHashtagsValidator, type: :validator do + let(:disallowed_tags) { [] } + describe '#validate' do before do - allow_any_instance_of(described_class).to receive(:select_tags) { tags } + disallowed_tags.each { |name| Fabricate(:tag, name: name, usable: false) } described_class.new.validate(status) end - let(:status) { double(errors: errors, local?: local, reblog?: reblog, text: '') } + let(:status) { double(errors: errors, local?: local, reblog?: reblog, text: disallowed_tags.map { |x| '#' + x }.join(' ')) } let(:errors) { double(add: nil) } - context 'unless status.local? && !status.reblog?' do + context 'for a remote reblog' do let(:local) { false } let(:reblog) { true } - it 'not calls errors.add' do + it 'does not add errors' do expect(errors).not_to have_received(:add).with(:text, any_args) end end - context 'status.local? && !status.reblog?' do + context 'for a local original status' do let(:local) { true } let(:reblog) { false } - context 'tags.empty?' do - let(:tags) { [] } + context 'when does not contain any disallowed hashtags' do + let(:disallowed_tags) { [] } - it 'not calls errors.add' do + it 'does not add errors' do expect(errors).not_to have_received(:add).with(:text, any_args) end end - context '!tags.empty?' do - let(:tags) { %w(a b c) } + context 'when contains disallowed hashtags' do + let(:disallowed_tags) { %w(a b c) } - it 'calls errors.add' do + it 'adds an error' do expect(errors).to have_received(:add) - .with(:text, I18n.t('statuses.disallowed_hashtags', tags: tags.join(', '), count: tags.size)) + .with(:text, I18n.t('statuses.disallowed_hashtags', tags: disallowed_tags.join(', '), count: disallowed_tags.size)) end end end -- cgit From e5cee8062f15191d9dd388a65f6caa104abfd559 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 16 Aug 2019 19:15:05 +0200 Subject: Fix blurhash and autoplay not working on public pages (#11585) --- app/controllers/home_controller.rb | 16 ---------------- app/controllers/public_timelines_controller.rb | 7 +------ app/controllers/shares_controller.rb | 18 +----------------- app/controllers/tags_controller.rb | 5 ----- app/helpers/application_helper.rb | 21 +++++++++++++++++++++ app/serializers/initial_state_serializer.rb | 5 +++++ app/views/home/index.html.haml | 3 +-- app/views/layouts/public.html.haml | 1 + app/views/public_timelines/show.html.haml | 1 - app/views/shares/show.html.haml | 2 +- app/views/tags/show.html.haml | 1 - spec/controllers/home_controller_spec.rb | 10 ---------- spec/controllers/shares_controller_spec.rb | 5 +---- 13 files changed, 32 insertions(+), 63 deletions(-) (limited to 'app/helpers') diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 22d507e77..7c8a18d17 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -3,7 +3,6 @@ class HomeController < ApplicationController before_action :authenticate_user! before_action :set_referrer_policy_header - before_action :set_initial_state_json def index @body_classes = 'app-body' @@ -39,21 +38,6 @@ class HomeController < ApplicationController redirect_to(matches ? tag_path(CGI.unescape(matches[:tag])) : default_redirect_path) end - def set_initial_state_json - serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer) - @initial_state_json = serializable_resource.to_json - end - - def initial_state_params - { - settings: Web::Setting.find_by(user: current_user)&.data || {}, - push_subscription: current_account.user.web_push_subscription(current_session), - current_account: current_account, - token: current_session.token, - admin: Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')), - } - end - def default_redirect_path if request.path.start_with?('/web') || whitelist_mode? new_user_session_path diff --git a/app/controllers/public_timelines_controller.rb b/app/controllers/public_timelines_controller.rb index 324bdc508..1332ba16c 100644 --- a/app/controllers/public_timelines_controller.rb +++ b/app/controllers/public_timelines_controller.rb @@ -8,12 +8,7 @@ class PublicTimelinesController < ApplicationController before_action :set_body_classes before_action :set_instance_presenter - def show - @initial_state_json = ActiveModelSerializers::SerializableResource.new( - InitialStatePresenter.new(settings: { known_fediverse: Setting.show_known_fediverse_at_about_page }, token: current_session&.token), - serializer: InitialStateSerializer - ).to_json - end + def show; end private diff --git a/app/controllers/shares_controller.rb b/app/controllers/shares_controller.rb index af605b98f..6546b8497 100644 --- a/app/controllers/shares_controller.rb +++ b/app/controllers/shares_controller.rb @@ -6,26 +6,10 @@ class SharesController < ApplicationController before_action :authenticate_user! before_action :set_body_classes - def show - serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer) - @initial_state_json = serializable_resource.to_json - end + def show; end private - def initial_state_params - text = [params[:title], params[:text], params[:url]].compact.join(' ') - - { - settings: Web::Setting.find_by(user: current_user)&.data || {}, - push_subscription: current_account.user.web_push_subscription(current_session), - current_account: current_account, - token: current_session.token, - admin: Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')), - text: text, - } - end - def set_body_classes @body_classes = 'modal-layout compose-standalone' end diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 5a6fcc8fd..4dfa05264 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -17,11 +17,6 @@ class TagsController < ApplicationController respond_to do |format| format.html do expires_in 0, public: true - - @initial_state_json = ActiveModelSerializers::SerializableResource.new( - InitialStatePresenter.new(settings: {}, token: current_session&.token), - serializer: InitialStateSerializer - ).to_json end format.rss do diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 9d113263d..23cbb1d93 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -122,4 +122,25 @@ module ApplicationHelper text = word_wrap(text, line_width: line_width - 2, break_sequence: break_sequence) text.split("\n").map { |line| '> ' + line }.join("\n") end + + def render_initial_state + state_params = { + settings: { + known_fediverse: Setting.show_known_fediverse_at_about_page, + }, + + text: [params[:title], params[:text], params[:url]].compact.join(' '), + } + + if user_signed_in? + state_params[:settings] = state_params[:settings].merge(Web::Setting.find_by(user: current_user)&.data || {}) + state_params[:push_subscription] = current_account.user.web_push_subscription(current_session) + state_params[:current_account] = current_account + state_params[:token] = current_session.token + state_params[:admin] = Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')) + end + + json = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(state_params), serializer: InitialStateSerializer).to_json + content_tag(:script, json_escape(json).html_safe, id: 'initial-state', type: 'application/json') + end end diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 2cebef2c0..fb53ea314 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -38,6 +38,11 @@ class InitialStateSerializer < ActiveModel::Serializer store[:use_pending_items] = object.current_account.user.setting_use_pending_items store[:is_staff] = object.current_account.user.staff? store[:trends] = Setting.trends && object.current_account.user.setting_trends + else + store[:auto_play_gif] = Setting.auto_play_gif + store[:display_media] = Setting.display_media + store[:reduce_motion] = Setting.reduce_motion + store[:use_blurhash] = Setting.use_blurhash end store diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index 4c7fac0b6..30c7aab19 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -5,8 +5,7 @@ = preload_link_tag asset_pack_path('features/notifications.js'), crossorigin: 'anonymous' %meta{name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key} - %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json) - + = render_initial_state = javascript_pack_tag 'application', integrity: true, crossorigin: 'anonymous' .app-holder#mastodon{ data: { props: Oj.dump(default_props) } } diff --git a/app/views/layouts/public.html.haml b/app/views/layouts/public.html.haml index 69738a2f7..b9179e23d 100644 --- a/app/views/layouts/public.html.haml +++ b/app/views/layouts/public.html.haml @@ -1,4 +1,5 @@ - content_for :header_tags do + = render_initial_state = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous' - content_for :content do diff --git a/app/views/public_timelines/show.html.haml b/app/views/public_timelines/show.html.haml index 913d5d855..07215efdf 100644 --- a/app/views/public_timelines/show.html.haml +++ b/app/views/public_timelines/show.html.haml @@ -3,7 +3,6 @@ - content_for :header_tags do %meta{ name: 'robots', content: 'noindex' }/ - %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json) = javascript_pack_tag 'about', integrity: true, crossorigin: 'anonymous' .page-header diff --git a/app/views/shares/show.html.haml b/app/views/shares/show.html.haml index 44b6f145f..f2f5479a7 100644 --- a/app/views/shares/show.html.haml +++ b/app/views/shares/show.html.haml @@ -1,5 +1,5 @@ - content_for :header_tags do - %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json) + = render_initial_state = javascript_pack_tag 'share', integrity: true, crossorigin: 'anonymous' #mastodon-compose{ data: { props: Oj.dump(default_props) } } diff --git a/app/views/tags/show.html.haml b/app/views/tags/show.html.haml index cf4246822..630702277 100644 --- a/app/views/tags/show.html.haml +++ b/app/views/tags/show.html.haml @@ -5,7 +5,6 @@ %meta{ name: 'robots', content: 'noindex' }/ %link{ rel: 'alternate', type: 'application/rss+xml', href: tag_url(@tag, format: 'rss') }/ - %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json) = javascript_pack_tag 'about', integrity: true, crossorigin: 'anonymous' = render 'og' diff --git a/spec/controllers/home_controller_spec.rb b/spec/controllers/home_controller_spec.rb index f43cf0c27..941f1dd91 100644 --- a/spec/controllers/home_controller_spec.rb +++ b/spec/controllers/home_controller_spec.rb @@ -27,16 +27,6 @@ RSpec.describe HomeController, type: :controller do subject expect(assigns(:body_classes)).to eq 'app-body' end - - it 'assigns @initial_state_json' do - subject - initial_state_json = json_str_to_hash(assigns(:initial_state_json)) - expect(initial_state_json[:meta]).to_not be_nil - expect(initial_state_json[:compose]).to_not be_nil - expect(initial_state_json[:accounts]).to_not be_nil - expect(initial_state_json[:settings]).to_not be_nil - expect(initial_state_json[:media_attachments]).to_not be_nil - end end end end diff --git a/spec/controllers/shares_controller_spec.rb b/spec/controllers/shares_controller_spec.rb index a74e9af56..d6de3016a 100644 --- a/spec/controllers/shares_controller_spec.rb +++ b/spec/controllers/shares_controller_spec.rb @@ -7,15 +7,12 @@ describe SharesController do before { sign_in user } describe 'GTE #show' do - subject(:initial_state_json) { JSON.parse(assigns(:initial_state_json), symbolize_names: true) } subject(:body_classes) { assigns(:body_classes) } before { get :show, params: { title: 'test title', text: 'test text', url: 'url1 url2' } } - it 'assigns json' do + it 'returns http success' do expect(response).to have_http_status :ok - expect(initial_state_json[:compose][:text]).to eq 'test title test text url1 url2' - expect(initial_state_json[:meta][:me]).to eq user.account.id.to_s expect(body_classes).to eq 'modal-layout compose-standalone' end end -- cgit From 987190417228bb56041ea824772341f07f4263d6 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 30 Aug 2019 07:41:16 +0200 Subject: Change layout of public profile directory to be the same as in web UI (#11705) --- app/controllers/directories_controller.rb | 2 +- app/helpers/statuses_helper.rb | 20 ++++++++++++ .../features/directory/components/account_card.js | 4 +-- app/javascript/styles/mastodon/components.scss | 7 ++++ app/javascript/styles/mastodon/containers.scss | 18 +++++++++++ app/views/directories/index.html.haml | 37 ++++++++++++++++++++-- 6 files changed, 83 insertions(+), 5 deletions(-) (limited to 'app/helpers') diff --git a/app/controllers/directories_controller.rb b/app/controllers/directories_controller.rb index 7244f02f0..7da975a23 100644 --- a/app/controllers/directories_controller.rb +++ b/app/controllers/directories_controller.rb @@ -28,7 +28,7 @@ class DirectoriesController < ApplicationController end def set_accounts - @accounts = Account.local.discoverable.by_recent_status.page(params[:page]).per(15).tap do |query| + @accounts = Account.local.discoverable.by_recent_status.page(params[:page]).per(20).tap do |query| query.merge!(Account.tagged_with(@tag.id)) if @tag query.merge!(Account.not_excluded_by_account(current_account)) if current_account end diff --git a/app/helpers/statuses_helper.rb b/app/helpers/statuses_helper.rb index e067380f6..8380b3c42 100644 --- a/app/helpers/statuses_helper.rb +++ b/app/helpers/statuses_helper.rb @@ -34,6 +34,26 @@ module StatusesHelper end end + def minimal_account_action_button(account) + if user_signed_in? + return if account.id == current_user.account_id + + if current_account.following?(account) || current_account.requested?(account) + link_to account_unfollow_path(account), class: 'icon-button active', data: { method: :post }, title: t('accounts.unfollow') do + fa_icon('user-times fw') + end + elsif !(account.memorial? || account.moved?) + link_to account_follow_path(account), class: "icon-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post }, title: t('accounts.follow') do + fa_icon('user-plus fw') + end + end + elsif !(account.memorial? || account.moved?) + link_to account_remote_follow_path(account), class: 'icon-button modal-button', target: '_new', title: t('accounts.follow') do + fa_icon('user-plus fw') + end + end + end + def svg_logo content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976') end diff --git a/app/javascript/mastodon/features/directory/components/account_card.js b/app/javascript/mastodon/features/directory/components/account_card.js index a9c9976be..cb23a02ba 100644 --- a/app/javascript/mastodon/features/directory/components/account_card.js +++ b/app/javascript/mastodon/features/directory/components/account_card.js @@ -119,7 +119,7 @@ class AccountCard extends ImmutablePureComponent { return (
- +
@@ -134,7 +134,7 @@ class AccountCard extends ImmutablePureComponent {
- {account.get('note').length > 0 && account.get('note') !== '

' &&
} +
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 1129680f1..d7e90fcaf 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -5367,6 +5367,7 @@ a.status-card.compact:hover { height: 125px; position: relative; background: darken($ui-base-color, 12%); + overflow: hidden; img { display: block; @@ -5388,6 +5389,7 @@ a.status-card.compact:hover { display: flex; align-items: center; text-decoration: none; + overflow: hidden; } &__relationship { @@ -5453,6 +5455,7 @@ a.status-card.compact:hover { padding: 15px 10px; border-bottom: 1px solid lighten($ui-base-color, 8%); width: 100%; + min-height: 18px + 30px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -5464,6 +5467,10 @@ a.status-card.compact:hover { display: inline; } } + + br { + display: none; + } } } } diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss index 2b6794ee2..e769c495b 100644 --- a/app/javascript/styles/mastodon/containers.scss +++ b/app/javascript/styles/mastodon/containers.scss @@ -763,6 +763,24 @@ } } + .directory__list { + display: grid; + grid-gap: 10px; + grid-template-columns: minmax(0, 50%) minmax(0, 50%); + + @media screen and (max-width: $no-gap-breakpoint) { + display: block; + } + + .icon-button { + font-size: 18px; + } + } + + .directory__card { + margin-bottom: 0; + } + .card-grid { display: flex; flex-wrap: wrap; diff --git a/app/views/directories/index.html.haml b/app/views/directories/index.html.haml index 54b27114c..30daa6bb1 100644 --- a/app/views/directories/index.html.haml +++ b/app/views/directories/index.html.haml @@ -17,7 +17,40 @@ - if @accounts.empty? = nothing_here - else - .card-grid - = render partial: 'application/card', collection: @accounts, as: :account + .directory__list + - @accounts.each do |account| + .directory__card + .directory__card__img + = image_tag account.header.url, alt: '' + .directory__card__bar + = link_to TagManager.instance.url_for(account), class: 'directory__card__bar__name' do + .avatar + = image_tag account.avatar.url, alt: '', width: 48, height: 48, class: 'u-photo' + + .display-name + %span{ id: "default_account_display_name", style: "display: none" }= account.username + %bdi + %strong.emojify.p-name= display_name(account, custom_emojify: true) + %span= acct(account) + .directory__card__bar__relationship.account__relationship + = minimal_account_action_button(account) + + .directory__card__extra + .account__header__content.emojify= Formatter.instance.simplified_format(account, custom_emojify: true) + + .directory__card__extra + .accounts-table__count + = number_to_human account.statuses_count, strip_insignificant_zeros: true + %small= t('accounts.posts', count: account.statuses_count).downcase + .accounts-table__count + = number_to_human account.followers_count, strip_insignificant_zeros: true + %small= t('accounts.followers', count: account.followers_count).downcase + .accounts-table__count + - if account.last_status_at.present? + %time.time-ago{ datetime: account.last_status_at.iso8601, title: l(account.last_status_at) }= l account.last_status_at + - else + = t('invites.expires_in_prompt') + + %small= t('accounts.last_active') = paginate @accounts -- cgit From 3221f998dd1fcfc2111178637fbb1f712d9e8388 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 3 Sep 2019 04:56:54 +0200 Subject: Change OpenGraph description on sign-up page to reflect invite (#11744) --- app/helpers/instance_helper.rb | 12 ++++++++++++ app/views/auth/registrations/new.html.haml | 2 +- app/views/shared/_og.html.haml | 4 ++-- config/locales/en.yml | 4 ++++ 4 files changed, 19 insertions(+), 3 deletions(-) (limited to 'app/helpers') diff --git a/app/helpers/instance_helper.rb b/app/helpers/instance_helper.rb index dd0b25f3e..daacb535b 100644 --- a/app/helpers/instance_helper.rb +++ b/app/helpers/instance_helper.rb @@ -8,4 +8,16 @@ module InstanceHelper def site_hostname @site_hostname ||= Addressable::URI.parse("//#{Rails.configuration.x.local_domain}").display_uri.host end + + def description_for_sign_up + prefix = begin + if @invite.present? + I18n.t('auth.description.prefix_invited_by_user', name: @invite.user.account.username) + else + I18n.t('auth.description.prefix_sign_up') + end + end + + safe_join([prefix, I18n.t('auth.description.suffix')], ' ') + end end diff --git a/app/views/auth/registrations/new.html.haml b/app/views/auth/registrations/new.html.haml index 83384d737..e807c8d86 100644 --- a/app/views/auth/registrations/new.html.haml +++ b/app/views/auth/registrations/new.html.haml @@ -2,7 +2,7 @@ = t('auth.register') - content_for :header_tags do - = render partial: 'shared/og' + = render partial: 'shared/og', locals: { description: description_for_sign_up } = simple_form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| = render 'shared/error_messages', object: resource diff --git a/app/views/shared/_og.html.haml b/app/views/shared/_og.html.haml index 67238fc8b..576f47a67 100644 --- a/app/views/shared/_og.html.haml +++ b/app/views/shared/_og.html.haml @@ -1,5 +1,5 @@ -- thumbnail = @instance_presenter.thumbnail -- description = strip_tags(@instance_presenter.site_short_description.presence || @instance_presenter.site_description.presence || t('about.about_mastodon_html')) +- thumbnail = @instance_presenter.thumbnail +- description ||= strip_tags(@instance_presenter.site_short_description.presence || @instance_presenter.site_description.presence || t('about.about_mastodon_html')) %meta{ name: 'description', content: description }/ diff --git a/config/locales/en.yml b/config/locales/en.yml index 9c9dbc94b..ad29e0a74 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -569,6 +569,10 @@ en: checkbox_agreement_without_rules_html: I agree to the terms of service delete_account: Delete account delete_account_html: If you wish to delete your account, you can proceed here. You will be asked for confirmation. + description: + prefix_invited_by_user: "@%{name} invites you to join this server of Mastodon!" + prefix_sign_up: Sign up on Mastodon today! + suffix: With an account, you will be able to follow people, post updates and exchange messages with users from any Mastodon server and more! didnt_get_confirmation: Didn't receive confirmation instructions? forgot_password: Forgot your password? invalid_reset_password_token: Password reset token is invalid or expired. Please request a new one. -- cgit From 17389c63c848e06b646a971c04ec055f371b92cb Mon Sep 17 00:00:00 2001 From: ThibG Date: Tue, 10 Sep 2019 20:56:07 +0200 Subject: Change /admin/custom_emoji to honor the auto_play_gif setting (#11801) Fixes #9535 --- app/helpers/application_helper.rb | 8 ++++++-- app/views/admin/custom_emojis/_custom_emoji.html.haml | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) (limited to 'app/helpers') diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 23cbb1d93..defd97609 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -77,8 +77,12 @@ module ApplicationHelper content_tag(:i, nil, attributes.merge(class: class_names.join(' '))) end - def custom_emoji_tag(custom_emoji) - image_tag(custom_emoji.image.url, class: 'emojione', alt: ":#{custom_emoji.shortcode}:") + def custom_emoji_tag(custom_emoji, animate = true) + if animate + image_tag(custom_emoji.image.url, class: 'emojione', alt: ":#{custom_emoji.shortcode}:") + else + image_tag(custom_emoji.image.url(:static), class: 'emojione custom-emoji', alt: ":#{custom_emoji.shortcode}", 'data-original' => full_asset_url(custom_emoji.image.url), 'data-static' => full_asset_url(custom_emoji.image.url(:static))) + end end def opengraph(property, content) diff --git a/app/views/admin/custom_emojis/_custom_emoji.html.haml b/app/views/admin/custom_emojis/_custom_emoji.html.haml index 9e06a3b42..2103b0fa7 100644 --- a/app/views/admin/custom_emojis/_custom_emoji.html.haml +++ b/app/views/admin/custom_emojis/_custom_emoji.html.haml @@ -3,7 +3,7 @@ = f.check_box :custom_emoji_ids, { multiple: true, include_hidden: false }, custom_emoji.id .batch-table__row__content.batch-table__row__content--with-image .batch-table__row__content__image - = custom_emoji_tag(custom_emoji) + = custom_emoji_tag(custom_emoji, animate = current_account&.user&.setting_auto_play_gif) .batch-table__row__content__text %samp= ":#{custom_emoji.shortcode}:" -- cgit From bc869501f5cf98a99b18f6d1bcab3b51db04008d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 13 Sep 2019 16:00:19 +0200 Subject: Fix name of Portuguese language not including Portugal (#11820) Fix #11602 --- app/helpers/settings_helper.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app/helpers') diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 92bc222ea..0cfde7edc 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -42,8 +42,8 @@ module SettingsHelper no: 'Norsk', oc: 'Occitan', pl: 'Polski', - pt: 'Português', - 'pt-BR': 'Português do Brasil', + pt: 'Português (Portugal)', + 'pt-BR': 'Português (Brasil)', ro: 'Română', ru: 'Русский', sk: 'Slovenčina', -- cgit From ef0d22f232723be035e95bde13310d02bf1c127b Mon Sep 17 00:00:00 2001 From: mayaeh Date: Mon, 16 Sep 2019 21:27:29 +0900 Subject: Add search and sort functions to hashtag admin UI (#11829) * Add search and sort functions to hashtag admin UI * Move scope processing from tags_controller to tag_filter * Fix based on method naming conventions * Fixed not to get 500 errors for invalid requests --- app/controllers/admin/tags_controller.rb | 15 +++-------- app/helpers/admin/filter_helper.rb | 2 +- app/models/tag.rb | 1 + app/models/tag_filter.rb | 44 ++++++++++++++++++++++++++++++++ app/views/admin/tags/index.html.haml | 32 ++++++++++++++++++----- config/locales/en.yml | 4 +++ config/locales/simple_form.en.yml | 2 ++ 7 files changed, 81 insertions(+), 19 deletions(-) create mode 100644 app/models/tag_filter.rb (limited to 'app/helpers') diff --git a/app/controllers/admin/tags_controller.rb b/app/controllers/admin/tags_controller.rb index 376ebe44d..65341bbfb 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/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb index 506429e10..8af1683e7 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/models/tag.rb b/app/models/tag.rb index a6aed0d68..4e77c404d 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -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 000000000..8921e186b --- /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/views/admin/tags/index.html.haml b/app/views/admin/tags/index.html.haml index 324d13d3e..cea1b71b5 100644 --- a/app/views/admin/tags/index.html.haml +++ b/app/views/admin/tags/index.html.haml @@ -8,16 +8,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/ diff --git a/config/locales/en.yml b/config/locales/en.yml index 8c9fe89f8..f05fdd48b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -521,6 +521,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 diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 2e5982de9..c542377a9 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -131,6 +131,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: -- cgit From 692fe477b4dded6a2f6f48685eef329ded4484ec Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 16 Sep 2019 15:11:01 +0200 Subject: New Crowdin translations (#11799) * New translations doorkeeper.en.yml (Hungarian) [ci skip] * New translations doorkeeper.en.yml (Ido) [ci skip] * New translations doorkeeper.en.yml (Indonesian) [ci skip] * New translations doorkeeper.en.yml (Italian) [ci skip] * New translations doorkeeper.en.yml (Japanese) [ci skip] * New translations doorkeeper.en.yml (Kazakh) [ci skip] * New translations simple_form.en.yml (Asturian) [ci skip] * New translations simple_form.en.yml (Arabic) [ci skip] * New translations en.json (Portuguese) [ci skip] * New translations en.json (Japanese) [ci skip] * New translations en.json (Kazakh) [ci skip] * New translations en.json (Korean) [ci skip] * New translations en.json (Latvian) [ci skip] * New translations en.json (Lithuanian) [ci skip] * New translations en.json (Malay) [ci skip] * New translations en.json (Norwegian) [ci skip] * New translations en.json (Occitan) [ci skip] * New translations en.json (Persian) [ci skip] * New translations en.json (Polish) [ci skip] * New translations en.json (Portuguese, Brazilian) [ci skip] * New translations en.json (Indonesian) [ci skip] * New translations en.json (Romanian) [ci skip] * New translations en.json (Russian) [ci skip] * New translations en.json (Serbian (Cyrillic)) [ci skip] * New translations en.json (Serbian (Latin)) [ci skip] * New translations en.json (Slovak) [ci skip] * New translations en.json (Slovenian) [ci skip] * New translations en.json (Spanish) [ci skip] * New translations en.json (Swedish) [ci skip] * New translations en.json (Tamil) [ci skip] * New translations en.json (Telugu) [ci skip] * New translations en.json (Thai) [ci skip] * New translations en.json (Italian) [ci skip] * New translations en.json (Ido) [ci skip] * New translations en.json (Ukrainian) [ci skip] * New translations en.json (Chinese Traditional, Hong Kong) [ci skip] * New translations simple_form.en.yml (Czech) [ci skip] * New translations en.json (Albanian) [ci skip] * New translations en.json (Arabic) [ci skip] * New translations en.json (Armenian) [ci skip] * New translations en.json (Asturian) [ci skip] * New translations en.json (Basque) [ci skip] * New translations en.json (Bengali) [ci skip] * New translations en.json (Bulgarian) [ci skip] * New translations en.json (Catalan) [ci skip] * New translations en.json (Chinese Simplified) [ci skip] * New translations en.json (Chinese Traditional) [ci skip] * New translations en.json (Corsican) [ci skip] * New translations en.json (Hungarian) [ci skip] * New translations en.json (Croatian) [ci skip] * New translations en.json (Danish) [ci skip] * New translations en.json (Dutch) [ci skip] * New translations en.json (Esperanto) [ci skip] * New translations en.json (Finnish) [ci skip] * New translations en.json (French) [ci skip] * New translations en.json (Galician) [ci skip] * New translations en.json (Georgian) [ci skip] * New translations en.json (German) [ci skip] * New translations en.json (Greek) [ci skip] * New translations en.json (Hebrew) [ci skip] * New translations en.json (Turkish) [ci skip] * New translations en.json (Welsh) [ci skip] * New translations simple_form.en.yml (Albanian) [ci skip] * New translations en.yml (Russian) [ci skip] * New translations en.yml (Korean) [ci skip] * New translations en.yml (Latvian) [ci skip] * New translations en.yml (Lithuanian) [ci skip] * New translations en.yml (Malay) [ci skip] * New translations en.yml (Norwegian) [ci skip] * New translations en.yml (Occitan) [ci skip] * New translations en.yml (Persian) [ci skip] * New translations en.yml (Polish) [ci skip] * New translations en.yml (Portuguese) [ci skip] * New translations en.yml (Portuguese, Brazilian) [ci skip] * New translations en.yml (Romanian) [ci skip] * New translations en.yml (Serbian (Cyrillic)) [ci skip] * New translations en.yml (Japanese) [ci skip] * New translations en.yml (Serbian (Latin)) [ci skip] * New translations en.yml (Slovak) [ci skip] * New translations en.yml (Slovenian) [ci skip] * New translations en.yml (Spanish) [ci skip] * New translations en.yml (Swedish) [ci skip] * New translations en.yml (Tamil) [ci skip] * New translations en.yml (Telugu) [ci skip] * New translations en.yml (Thai) [ci skip] * New translations en.yml (Turkish) [ci skip] * New translations en.yml (Ukrainian) [ci skip] * New translations en.yml (Welsh) [ci skip] * New translations en.yml (Kazakh) [ci skip] * New translations en.yml (Italian) [ci skip] * New translations en.yml (Albanian) [ci skip] * New translations en.yml (Croatian) [ci skip] * New translations en.yml (Arabic) [ci skip] * New translations en.yml (Armenian) [ci skip] * New translations en.yml (Asturian) [ci skip] * New translations en.yml (Basque) [ci skip] * New translations en.yml (Bengali) [ci skip] * New translations en.yml (Bulgarian) [ci skip] * New translations en.yml (Catalan) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations en.yml (Chinese Traditional) [ci skip] * New translations en.yml (Chinese Traditional, Hong Kong) [ci skip] * New translations en.yml (Corsican) [ci skip] * New translations en.yml (Danish) [ci skip] * New translations en.yml (Indonesian) [ci skip] * New translations en.yml (Dutch) [ci skip] * New translations en.yml (Esperanto) [ci skip] * New translations en.yml (Finnish) [ci skip] * New translations en.yml (French) [ci skip] * New translations en.yml (Galician) [ci skip] * New translations en.yml (Georgian) [ci skip] * New translations en.yml (German) [ci skip] * New translations en.yml (Greek) [ci skip] * New translations en.yml (Hebrew) [ci skip] * New translations en.yml (Hungarian) [ci skip] * New translations en.yml (Ido) [ci skip] * New translations doorkeeper.en.yml (Estonian) [ci skip] * New translations en.json (Spanish) [ci skip] * New translations en.yml (Czech) [ci skip] * New translations en.json (Slovak) [ci skip] * New translations en.yml (Slovak) [ci skip] * New translations en.yml (Slovak) [ci skip] * New translations en.json (Russian) [ci skip] * New translations en.json (Russian) [ci skip] * New translations en.yml (Russian) [ci skip] * New translations simple_form.en.yml (Russian) [ci skip] * New translations en.yml (Corsican) [ci skip] * New translations en.json (Korean) [ci skip] * New translations en.json (Thai) [ci skip] * New translations en.yml (Thai) [ci skip] * New translations en.json (Spanish) [ci skip] * New translations en.yml (Spanish) [ci skip] * New translations simple_form.en.yml (Spanish) [ci skip] * New translations en.json (Spanish) [ci skip] * New translations en.yml (German) [ci skip] * New translations en.json (Norwegian Nynorsk) [ci skip] * New translations en.yml (Norwegian Nynorsk) [ci skip] * New translations simple_form.en.yml (Norwegian Nynorsk) [ci skip] * New translations activerecord.en.yml (Norwegian Nynorsk) [ci skip] * New translations devise.en.yml (Norwegian Nynorsk) [ci skip] * New translations doorkeeper.en.yml (Norwegian Nynorsk) [ci skip] * New translations en.json (Armenian) [ci skip] * New translations en.json (Breton) [ci skip] * New translations en.json (Bulgarian) [ci skip] * New translations doorkeeper.en.yml (Breton) [ci skip] * New translations devise.en.yml (Breton) [ci skip] * New translations simple_form.en.yml (Breton) [ci skip] * New translations en.yml (Breton) [ci skip] * New translations activerecord.en.yml (Breton) [ci skip] * New translations devise.en.yml (Asturian) [ci skip] * New translations en.json (Asturian) [ci skip] * New translations activerecord.en.yml (Asturian) [ci skip] * New translations devise.en.yml (Russian) [ci skip] * New translations activerecord.en.yml (Russian) [ci skip] * New translations simple_form.en.yml (Russian) [ci skip] * New translations devise.en.yml (Norwegian) [ci skip] * New translations activerecord.en.yml (Norwegian) [ci skip] * New translations en.json (Norwegian) [ci skip] * New translations en.json (Malay) [ci skip] * New translations en.json (Serbian (Cyrillic)) [ci skip] * New translations devise.en.yml (Serbian (Latin)) [ci skip] * New translations devise.en.yml (Ukrainian) [ci skip] * New translations activerecord.en.yml (Ukrainian) [ci skip] * New translations en.yml (Ukrainian) [ci skip] * New translations devise.en.yml (Slovak) [ci skip] * New translations activerecord.en.yml (Slovak) [ci skip] * New translations en.json (Slovak) [ci skip] * New translations activerecord.en.yml (Serbian (Latin)) [ci skip] * New translations en.json (Serbian (Latin)) [ci skip] * New translations devise.en.yml (Serbian (Cyrillic)) [ci skip] * New translations activerecord.en.yml (Serbian (Cyrillic)) [ci skip] * New translations activerecord.en.yml (Indonesian) [ci skip] * New translations devise.en.yml (Georgian) [ci skip] * New translations activerecord.en.yml (Georgian) [ci skip] * New translations en.json (Georgian) [ci skip] * New translations devise.en.yml (Croatian) [ci skip] * New translations en.json (Croatian) [ci skip] * New translations en.json (Hebrew) [ci skip] * New translations devise.en.yml (Chinese Traditional, Hong Kong) [ci skip] * New translations activerecord.en.yml (Chinese Traditional, Hong Kong) [ci skip] * New translations en.json (Chinese Traditional, Hong Kong) [ci skip] * New translations devise.en.yml (Bulgarian) [ci skip] * New translations activerecord.en.yml (Bulgarian) [ci skip] * New translations en.json (Lithuanian) [ci skip] * New translations en.json (Latvian) [ci skip] * New translations devise.en.yml (Indonesian) [ci skip] * New translations en.json (Indonesian) [ci skip] * New translations devise.en.yml (Ido) [ci skip] * New translations en.json (Ido) [ci skip] * New translations devise.en.yml (Hebrew) [ci skip] * New translations activerecord.en.yml (Hebrew) [ci skip] * New translations en.yml (Slovak) [ci skip] * New translations en.yml (Slovak) [ci skip] * New translations en.yml (French) [ci skip] * New translations en.json (Norwegian Nynorsk) [ci skip] * New translations en.json (Norwegian Nynorsk) [ci skip] * New translations en.json (Norwegian Nynorsk) [ci skip] * New translations en.json (Norwegian Nynorsk) [ci skip] * New translations en.json (Norwegian Nynorsk) [ci skip] * New translations en.json (Arabic) [ci skip] * New translations en.yml (Arabic) [ci skip] * New translations en.json (Norwegian Nynorsk) [ci skip] * New translations en.yml (Greek) [ci skip] * New translations en.yml (Hungarian) [ci skip] * New translations en.yml (Italian) [ci skip] * New translations en.yml (Japanese) [ci skip] * New translations en.yml (Kazakh) [ci skip] * New translations en.yml (Korean) [ci skip] * New translations en.yml (Dutch) [ci skip] * New translations en.yml (Esperanto) [ci skip] * New translations en.yml (Estonian) [ci skip] * New translations en.yml (German) [ci skip] * New translations en.yml (Finnish) [ci skip] * New translations en.yml (Galician) [ci skip] * New translations en.yml (Danish) [ci skip] * New translations en.yml (Swedish) [ci skip] * New translations en.yml (Spanish) [ci skip] * New translations en.yml (Welsh) [ci skip] * New translations en.yml (Occitan) [ci skip] * New translations en.yml (Portuguese) [ci skip] * New translations en.yml (Persian) [ci skip] * New translations en.yml (Polish) [ci skip] * New translations en.json (Portuguese) [ci skip] * New translations simple_form.en.yml (Portuguese) [ci skip] * New translations doorkeeper.en.yml (Portuguese) [ci skip] * New translations en.yml (Portuguese, Brazilian) [ci skip] * New translations en.yml (Romanian) [ci skip] * New translations en.yml (Slovenian) [ci skip] * New translations en.yml (Lithuanian) [ci skip] * New translations en.yml (Norwegian) [ci skip] * New translations en.yml (Asturian) [ci skip] * New translations en.yml (Chinese Traditional, Hong Kong) [ci skip] * New translations en.yml (Georgian) [ci skip] * New translations en.yml (Russian) [ci skip] * New translations en.yml (Chinese Simplified) [ci skip] * New translations en.yml (Catalan) [ci skip] * New translations en.yml (Chinese Traditional) [ci skip] * New translations en.yml (Corsican) [ci skip] * New translations en.yml (Czech) [ci skip] * New translations en.yml (Basque) [ci skip] * New translations en.yml (Serbian (Cyrillic)) [ci skip] * New translations en.yml (Serbian (Latin)) [ci skip] * New translations en.yml (Slovak) [ci skip] * New translations en.yml (Ukrainian) [ci skip] * New translations en.yml (French) [ci skip] * New translations en.yml (Arabic) [ci skip] * New translations en.yml (Albanian) [ci skip] * New translations en.yml (Danish) [ci skip] * i18n-tasks normalize * yarn manage:translations --- app/helpers/settings_helper.rb | 3 +- app/javascript/mastodon/locales/ar.json | 7 +- app/javascript/mastodon/locales/ast.json | 1 + app/javascript/mastodon/locales/bg.json | 1 + app/javascript/mastodon/locales/bn.json | 1 + app/javascript/mastodon/locales/br.json | 414 ++++++++++ app/javascript/mastodon/locales/ca.json | 1 + app/javascript/mastodon/locales/co.json | 1 + app/javascript/mastodon/locales/cs.json | 1 + app/javascript/mastodon/locales/cy.json | 1 + app/javascript/mastodon/locales/da.json | 1 + app/javascript/mastodon/locales/de.json | 1 + .../mastodon/locales/defaultMessages.json | 40 +- app/javascript/mastodon/locales/el.json | 1 + app/javascript/mastodon/locales/en.json | 3 + app/javascript/mastodon/locales/eo.json | 1 + app/javascript/mastodon/locales/es.json | 41 +- app/javascript/mastodon/locales/et.json | 1 + app/javascript/mastodon/locales/eu.json | 1 + app/javascript/mastodon/locales/fa.json | 1 + app/javascript/mastodon/locales/fi.json | 1 + app/javascript/mastodon/locales/fr.json | 1 + app/javascript/mastodon/locales/gl.json | 1 + app/javascript/mastodon/locales/he.json | 1 + app/javascript/mastodon/locales/hi.json | 1 + app/javascript/mastodon/locales/hr.json | 1 + app/javascript/mastodon/locales/hu.json | 1 + app/javascript/mastodon/locales/hy.json | 1 + app/javascript/mastodon/locales/id.json | 1 + app/javascript/mastodon/locales/io.json | 1 + app/javascript/mastodon/locales/it.json | 1 + app/javascript/mastodon/locales/ja.json | 1 + app/javascript/mastodon/locales/ka.json | 1 + app/javascript/mastodon/locales/kk.json | 1 + app/javascript/mastodon/locales/ko.json | 3 +- app/javascript/mastodon/locales/lt.json | 1 + app/javascript/mastodon/locales/lv.json | 1 + app/javascript/mastodon/locales/ms.json | 1 + app/javascript/mastodon/locales/nl.json | 1 + app/javascript/mastodon/locales/nn.json | 414 ++++++++++ app/javascript/mastodon/locales/no.json | 1 + app/javascript/mastodon/locales/oc.json | 1 + app/javascript/mastodon/locales/pl.json | 1 + app/javascript/mastodon/locales/pt-BR.json | 1 + app/javascript/mastodon/locales/pt-PT.json | 414 ++++++++++ app/javascript/mastodon/locales/pt.json | 413 ---------- app/javascript/mastodon/locales/ro.json | 1 + app/javascript/mastodon/locales/ru.json | 7 +- app/javascript/mastodon/locales/sk.json | 1 + app/javascript/mastodon/locales/sl.json | 1 + app/javascript/mastodon/locales/sq.json | 1 + app/javascript/mastodon/locales/sr-Latn.json | 1 + app/javascript/mastodon/locales/sr.json | 1 + app/javascript/mastodon/locales/sv.json | 1 + app/javascript/mastodon/locales/ta.json | 1 + app/javascript/mastodon/locales/te.json | 1 + app/javascript/mastodon/locales/th.json | 11 +- app/javascript/mastodon/locales/tr.json | 1 + app/javascript/mastodon/locales/uk.json | 1 + app/javascript/mastodon/locales/whitelist_br.json | 2 + app/javascript/mastodon/locales/whitelist_nn.json | 2 + .../mastodon/locales/whitelist_pt-PT.json | 2 + app/javascript/mastodon/locales/whitelist_pt.json | 2 - app/javascript/mastodon/locales/zh-CN.json | 1 + app/javascript/mastodon/locales/zh-HK.json | 1 + app/javascript/mastodon/locales/zh-TW.json | 1 + config/application.rb | 2 +- config/locales/activerecord.br.yml | 1 + config/locales/activerecord.nn.yml | 1 + config/locales/activerecord.pt.yml | 13 - config/locales/ar.yml | 4 +- config/locales/ast.yml | 1 - config/locales/br.yml | 20 + config/locales/ca.yml | 1 - config/locales/co.yml | 4 +- config/locales/cs.yml | 9 +- config/locales/cy.yml | 1 - config/locales/da.yml | 2 +- config/locales/de.yml | 8 +- config/locales/devise.br.yml | 1 + config/locales/devise.nn.yml | 1 + config/locales/devise.pt.yml | 83 -- config/locales/doorkeeper.br.yml | 1 + config/locales/doorkeeper.nn.yml | 1 + config/locales/doorkeeper.pt-PT.yml | 118 +++ config/locales/doorkeeper.pt.yml | 118 --- config/locales/el.yml | 1 - config/locales/eo.yml | 1 - config/locales/es.yml | 8 +- config/locales/et.yml | 1 - config/locales/eu.yml | 1 - config/locales/fa.yml | 1 - config/locales/fi.yml | 1 - config/locales/fr.yml | 3 +- config/locales/gl.yml | 1 - config/locales/hu.yml | 1 - config/locales/it.yml | 1 - config/locales/ja.yml | 1 - config/locales/ka.yml | 1 - config/locales/kk.yml | 1 - config/locales/ko.yml | 1 - config/locales/lt.yml | 1 - config/locales/nl.yml | 1 - config/locales/nn.yml | 20 + config/locales/no.yml | 1 - config/locales/oc.yml | 1 - config/locales/pl.yml | 1 - config/locales/pt-BR.yml | 1 - config/locales/pt-PT.yml | 911 ++++++++++++++++++++ config/locales/pt.yml | 912 --------------------- config/locales/ro.yml | 1 - config/locales/ru.yml | 5 +- config/locales/simple_form.br.yml | 1 + config/locales/simple_form.es.yml | 12 + config/locales/simple_form.nn.yml | 1 + config/locales/simple_form.pt-PT.yml | 127 +++ config/locales/simple_form.pt.yml | 127 --- config/locales/simple_form.ru.yml | 6 +- config/locales/sk.yml | 19 +- config/locales/sl.yml | 1 - config/locales/sq.yml | 1 - config/locales/sr-Latn.yml | 1 - config/locales/sr.yml | 1 - config/locales/sv.yml | 1 - config/locales/th.yml | 1 + config/locales/uk.yml | 1 - config/locales/zh-CN.yml | 1 - config/locales/zh-HK.yml | 1 - config/locales/zh-TW.yml | 1 - 129 files changed, 2630 insertions(+), 1770 deletions(-) create mode 100644 app/javascript/mastodon/locales/br.json create mode 100644 app/javascript/mastodon/locales/nn.json create mode 100644 app/javascript/mastodon/locales/pt-PT.json delete mode 100644 app/javascript/mastodon/locales/pt.json create mode 100644 app/javascript/mastodon/locales/whitelist_br.json create mode 100644 app/javascript/mastodon/locales/whitelist_nn.json create mode 100644 app/javascript/mastodon/locales/whitelist_pt-PT.json delete mode 100644 app/javascript/mastodon/locales/whitelist_pt.json create mode 100644 config/locales/activerecord.br.yml create mode 100644 config/locales/activerecord.nn.yml delete mode 100644 config/locales/activerecord.pt.yml create mode 100644 config/locales/br.yml create mode 100644 config/locales/devise.br.yml create mode 100644 config/locales/devise.nn.yml delete mode 100644 config/locales/devise.pt.yml create mode 100644 config/locales/doorkeeper.br.yml create mode 100644 config/locales/doorkeeper.nn.yml create mode 100644 config/locales/doorkeeper.pt-PT.yml delete mode 100644 config/locales/doorkeeper.pt.yml create mode 100644 config/locales/nn.yml create mode 100644 config/locales/pt-PT.yml delete mode 100644 config/locales/pt.yml create mode 100644 config/locales/simple_form.br.yml create mode 100644 config/locales/simple_form.nn.yml create mode 100644 config/locales/simple_form.pt-PT.yml delete mode 100644 config/locales/simple_form.pt.yml (limited to 'app/helpers') diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 0cfde7edc..2b3fd1263 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/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json index 6424cd1a7..ce66a5d1c 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 ef17d6d64..2ef693fcb 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 b0954f199..309f04513 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 241b43573..e4984a118 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 000000000..2de037f16 --- /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 4554500f5..77f84ac7d 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 b54857e36..d95f32b18 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 b3d1e8157..8acf27cb3 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 bc65d601e..cdf2656d7 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 dff8c3c05..14b0f7563 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 a9b777c03..845bc5156 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 b3c25ebe6..db68f18c5 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -1132,28 +1132,24 @@ { "descriptors": [ { - "defaultMessage": "Delete", - "id": "upload_form.undo" - }, - { - "defaultMessage": "Edit", - "id": "upload_form.edit" + "defaultMessage": "Uploading...", + "id": "upload_progress.label" } ], - "path": "app/javascript/mastodon/features/compose/components/upload.json" + "path": "app/javascript/mastodon/features/compose/components/upload_progress.json" }, { "descriptors": [ { - "defaultMessage": "Are you sure you want to log out?", - "id": "confirmations.logout.message" + "defaultMessage": "Delete", + "id": "upload_form.undo" }, { - "defaultMessage": "Log out", - "id": "confirmations.logout.confirm" + "defaultMessage": "Edit", + "id": "upload_form.edit" } ], - "path": "app/javascript/mastodon/features/compose/containers/navigation_container.json" + "path": "app/javascript/mastodon/features/compose/components/upload.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 4c8a58778..bdd1da36c 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 debc755c3..e959e5188 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", @@ -169,6 +170,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}}", @@ -262,6 +264,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 a04a70cce..31750050e 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 3b36571b1..a033f6e1f 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 63253a177..1d1dfd35a 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 e88bcfff1..f1fc17fdd 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 632698c46..9382ec5ee 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 8f8e9fc58..01b5edad1 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 72158c413..7cfe9829a 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 1bf37c898..3cc44f43e 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 fd7e40c53..b6cc3e6ce 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 55b383d59..e2d1eb49d 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 8d7cb436c..6daabc694 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 513f2a22a..f5a02065b 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 1c3f1eec0..1484c76df 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 5e1f318be..c9e48a1a6 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 afbd970ec..6c1b7fa8b 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 caabf6ef3..dc43bcb5c 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 1960dafba..4fb34e772 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 d3018c0bf..e2a6ee6c6 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 5d671d907..a07302f0a 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 50f7ca543..3ec9a8a16 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 7d0776dff..2de037f16 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 d9b125695..8d281c9d5 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 b83d26a0a..9bd5eef72 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 439dccbb3..73e7b3905 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 000000000..dda402494 --- /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 77ddad7e0..8fc722037 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 10501796d..d5abe89fb 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 e6c82c4f3..62f75e3c2 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 e11141f6c..debf9e6f6 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-PT.json b/app/javascript/mastodon/locales/pt-PT.json new file mode 100644 index 000000000..feba8fd9a --- /dev/null +++ b/app/javascript/mastodon/locales/pt-PT.json @@ -0,0 +1,414 @@ +{ + "account.add_or_remove_from_list": "Adicionar ou remover das listas", + "account.badges.bot": "Robô", + "account.block": "Bloquear @{name}", + "account.block_domain": "Esconder tudo do domínio {domain}", + "account.blocked": "Bloqueado", + "account.cancel_follow_request": "Cancel follow request", + "account.direct": "Mensagem directa @{name}", + "account.domain_blocked": "Domínio escondido", + "account.edit_profile": "Editar perfil", + "account.endorse": "Atributo no perfil", + "account.follow": "Seguir", + "account.followers": "Seguidores", + "account.followers.empty": "Ainda ninguém segue este utilizador.", + "account.follows": "Segue", + "account.follows.empty": "Este utilizador ainda não segue alguém.", + "account.follows_you": "É teu seguidor", + "account.hide_reblogs": "Esconder partilhas de @{name}", + "account.last_status": "Last active", + "account.link_verified_on": "A posse deste link foi verificada em {date}", + "account.locked_info": "O estatuto de privacidade desta conta é fechado. O dono revê manualmente que a pode seguir.", + "account.media": "Média", + "account.mention": "Mencionar @{name}", + "account.moved_to": "{name} mudou a sua conta para:", + "account.mute": "Silenciar @{name}", + "account.mute_notifications": "Silenciar notificações de @{name}", + "account.muted": "Silenciada", + "account.never_active": "Never", + "account.posts": "Publicações", + "account.posts_with_replies": "Publicações e respostas", + "account.report": "Denunciar @{name}", + "account.requested": "A aguardar aprovação. Clique para cancelar o pedido de seguimento", + "account.share": "Partilhar o perfil @{name}", + "account.show_reblogs": "Mostrar partilhas de @{name}", + "account.unblock": "Desbloquear @{name}", + "account.unblock_domain": "Mostrar {domain}", + "account.unendorse": "Não mostrar no perfil", + "account.unfollow": "Deixar de seguir", + "account.unmute": "Não silenciar @{name}", + "account.unmute_notifications": "Deixar de silenciar @{name}", + "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", + "alert.rate_limited.title": "Rate limited", + "alert.unexpected.message": "Ocorreu um erro inesperado.", + "alert.unexpected.title": "Bolas!", + "autosuggest_hashtag.per_week": "{count} per week", + "boost_modal.combo": "Pode clicar {combo} para não voltar a ver", + "bundle_column_error.body": "Algo de errado aconteceu enquanto este componente era carregado.", + "bundle_column_error.retry": "Tente de novo", + "bundle_column_error.title": "Erro de rede", + "bundle_modal_error.close": "Fechar", + "bundle_modal_error.message": "Algo de errado aconteceu enquanto este componente era carregado.", + "bundle_modal_error.retry": "Tente de novo", + "column.blocks": "Utilizadores Bloqueados", + "column.community": "Cronologia local", + "column.direct": "Mensagens directas", + "column.directory": "Browse profiles", + "column.domain_blocks": "Domínios escondidos", + "column.favourites": "Favoritos", + "column.follow_requests": "Seguidores pendentes", + "column.home": "Início", + "column.lists": "Listas", + "column.mutes": "Utilizadores silenciados", + "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", + "column_header.moveRight_settings": "Mover coluna para a direita", + "column_header.pin": "Fixar", + "column_header.show_settings": "Mostrar configurações", + "column_header.unpin": "Desafixar", + "column_subheading.settings": "Configurações", + "community.column_settings.media_only": "Somente multimédia", + "compose_form.direct_message_warning": "Esta publicação será enviada apenas para os utilizadores mencionados.", + "compose_form.direct_message_warning_learn_more": "Conhecer mais", + "compose_form.hashtag_warning": "Este toot não será listado em nenhuma hashtag por ser não listado. Apenas toots públics podem ser pesquisados por hashtag.", + "compose_form.lock_disclaimer": "A tua conta não está {locked}. Qualquer pessoa pode seguir-te e ver as publicações direcionadas apenas a seguidores.", + "compose_form.lock_disclaimer.lock": "bloqueado", + "compose_form.placeholder": "Em que estás a pensar?", + "compose_form.poll.add_option": "Adicionar uma opção", + "compose_form.poll.duration": "Duração da votação", + "compose_form.poll.option_placeholder": "Opção {number}", + "compose_form.poll.remove_option": "Eliminar esta opção", + "compose_form.publish": "Toot", + "compose_form.publish_loud": "{publish}!", + "compose_form.sensitive.hide": "Marcar multimédia como sensível", + "compose_form.sensitive.marked": "Média marcada como sensível", + "compose_form.sensitive.unmarked": "Média não está marcada como sensível", + "compose_form.spoiler.marked": "Texto escondido atrás de aviso", + "compose_form.spoiler.unmarked": "O texto não está escondido", + "compose_form.spoiler_placeholder": "Escreve o teu aviso aqui", + "confirmation_modal.cancel": "Cancelar", + "confirmations.block.block_and_report": "Bloquear e denunciar", + "confirmations.block.confirm": "Bloquear", + "confirmations.block.message": "De certeza que queres bloquear {name}?", + "confirmations.delete.confirm": "Eliminar", + "confirmations.delete.message": "De certeza que queres eliminar esta publicação?", + "confirmations.delete_list.confirm": "Eliminar", + "confirmations.delete_list.message": "Tens a certeza de que desejas eliminar permanentemente esta lista?", + "confirmations.domain_block.confirm": "Esconder tudo deste domínio", + "confirmations.domain_block.message": "De certeza que queres bloquear completamente o domínio {domain}? Na maioria dos casos, silenciar ou bloquear alguns utilizadores é suficiente e é o recomendado. Não irás ver conteúdo daquele domínio em cronologia alguma nem nas tuas notificações. Os teus seguidores daquele domínio serão removidos.", + "confirmations.logout.confirm": "Log out", + "confirmations.logout.message": "Are you sure you want to log out?", + "confirmations.mute.confirm": "Silenciar", + "confirmations.mute.message": "De certeza que queres silenciar {name}?", + "confirmations.redraft.confirm": "Apagar & redigir", + "confirmations.redraft.message": "Tens a certeza que queres apagar e redigir esta publicação? Os favoritos e as partilhas perder-se-ão e as respostas à publicação original ficarão órfãs.", + "confirmations.reply.confirm": "Responder", + "confirmations.reply.message": "Responder agora irá reescrever a mensagem que estás a compor actualmente. Tens a certeza que queres continuar?", + "confirmations.unfollow.confirm": "Deixar de seguir", + "confirmations.unfollow.message": "De certeza que queres deixar de seguir {name}?", + "directory.federated": "From known fediverse", + "directory.local": "From {domain} only", + "directory.new_arrivals": "New arrivals", + "directory.recently_active": "Recently active", + "embed.instructions": "Publica esta publicação no teu site copiando o código abaixo.", + "embed.preview": "Podes ver aqui como irá ficar:", + "emoji_button.activity": "Actividade", + "emoji_button.custom": "Personalizar", + "emoji_button.flags": "Bandeiras", + "emoji_button.food": "Comida & Bebida", + "emoji_button.label": "Inserir Emoji", + "emoji_button.nature": "Natureza", + "emoji_button.not_found": "Não tem emojis!! (╯°□°)╯︵ ┻━┻", + "emoji_button.objects": "Objectos", + "emoji_button.people": "Pessoas", + "emoji_button.recent": "Utilizados regularmente", + "emoji_button.search": "Pesquisar...", + "emoji_button.search_results": "Resultados da pesquisa", + "emoji_button.symbols": "Símbolos", + "emoji_button.travel": "Viagens & Lugares", + "empty_column.account_timeline": "Sem toots por aqui!", + "empty_column.account_unavailable": "Perfil indisponível", + "empty_column.blocks": "Ainda não bloqueaste qualquer utilizador.", + "empty_column.community": "A timeline local está vazia. Escreve algo publicamente para começar!", + "empty_column.direct": "Ainda não tens qualquer mensagem directa. Quando enviares ou receberes alguma, ela irá aparecer aqui.", + "empty_column.domain_blocks": "Ainda não há qualquer domínio escondido.", + "empty_column.favourited_statuses": "Ainda não tens quaisquer toots favoritos. Quando tiveres algum, ele irá aparecer aqui.", + "empty_column.favourites": "Ainda ninguém marcou este toot como favorito. Quando alguém o fizer, ele irá aparecer aqui.", + "empty_column.follow_requests": "Ainda não tens nenhum pedido de seguimento. Quando receberes algum, ele irá aparecer aqui.", + "empty_column.hashtag": "Não foram encontradas publicações com essa hashtag.", + "empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.", + "empty_column.home.public_timeline": "Cronologia pública", + "empty_column.list": "Ainda não existem publicações nesta lista. Quando membros desta lista fizerem novas publicações, elas aparecerão aqui.", + "empty_column.lists": "Ainda não tens qualquer lista. Quando criares uma, ela irá aparecer aqui.", + "empty_column.mutes": "Ainda não silenciaste qualquer utilizador.", + "empty_column.notifications": "Não tens notificações. Interage com outros utilizadores para iniciar uma conversa.", + "empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para veres aqui os conteúdos públicos", + "follow_request.authorize": "Autorizar", + "follow_request.reject": "Rejeitar", + "getting_started.developers": "Responsáveis pelo desenvolvimento", + "getting_started.directory": "Directório de perfil", + "getting_started.documentation": "Documentação", + "getting_started.heading": "Primeiros passos", + "getting_started.invite": "Convidar pessoas", + "getting_started.open_source_notice": "Mastodon é software de código aberto (open source). Podes contribuir ou reportar problemas no GitHub do projecto: {github}.", + "getting_started.security": "Segurança", + "getting_started.terms": "Termos de serviço", + "hashtag.column_header.tag_mode.all": "e {additional}", + "hashtag.column_header.tag_mode.any": "ou {additional}", + "hashtag.column_header.tag_mode.none": "sem {additional}", + "hashtag.column_settings.select.no_options_message": "Não foram encontradas sugestões", + "hashtag.column_settings.select.placeholder": "Introduzir as hashtags…", + "hashtag.column_settings.tag_mode.all": "Todos estes", + "hashtag.column_settings.tag_mode.any": "Qualquer destes", + "hashtag.column_settings.tag_mode.none": "Nenhum destes", + "hashtag.column_settings.tag_toggle": "Incluir etiquetas adicionais para esta coluna", + "home.column_settings.basic": "Básico", + "home.column_settings.show_reblogs": "Mostrar boosts", + "home.column_settings.show_replies": "Mostrar respostas", + "home.column_settings.update_live": "Update in real-time", + "intervals.full.days": "{number, plural, one {# dia} other {# dias}}", + "intervals.full.hours": "{number, plural, one {# hora} other {# horas}}", + "intervals.full.minutes": "{number, plural, one {# minuto} other {# minutos}}", + "introduction.federation.action": "Seguinte", + "introduction.federation.federated.headline": "Federada", + "introduction.federation.federated.text": "Publicações públicas de outros servidores do fediverse aparecerão na cronologia federativa.", + "introduction.federation.home.headline": "Início", + "introduction.federation.home.text": "As publicações das pessoas que tu segues aparecerão na tua coluna inicial. Tu podes seguir qualquer pessoa em qualquer servidor!", + "introduction.federation.local.headline": "Local", + "introduction.federation.local.text": "Publicações públicas de pessoas que tu segues no teu servidor aparecerão na coluna local.", + "introduction.interactions.action": "Terminar o tutorial!", + "introduction.interactions.favourite.headline": "Favorito", + "introduction.interactions.favourite.text": "Podes guardar um toot para depois e deixar o autor saber que gostaste dele, marcando-o como favorito.", + "introduction.interactions.reblog.headline": "Boost", + "introduction.interactions.reblog.text": "Podes partilhar os toots de outras pessoas com os teus seguidores partilhando-os.", + "introduction.interactions.reply.headline": "Responder", + "introduction.interactions.reply.text": "Tu podes responder a toots de outras pessoas e aos teus, o que os irá juntar numa conversa.", + "introduction.welcome.action": "Vamos!", + "introduction.welcome.headline": "Primeiros passos", + "introduction.welcome.text": "Bem-vindo ao fediverso! Em pouco tempo poderás enviar mensagens e falar com os teus amigos numa grande variedade de servidores. Mas este servidor, {domain}, é especial—ele alberga o teu perfil. Por isso, lembra-te do seu nome.", + "keyboard_shortcuts.back": "para voltar", + "keyboard_shortcuts.blocked": "para abrir a lista de utilizadores bloqueados", + "keyboard_shortcuts.boost": "para partilhar", + "keyboard_shortcuts.column": "para focar uma publicação numa das colunas", + "keyboard_shortcuts.compose": "para focar na área de publicação", + "keyboard_shortcuts.description": "Descrição", + "keyboard_shortcuts.direct": "para abrir a coluna das mensagens directas", + "keyboard_shortcuts.down": "para mover para baixo na lista", + "keyboard_shortcuts.enter": "para expandir um estado", + "keyboard_shortcuts.favourite": "para adicionar aos favoritos", + "keyboard_shortcuts.favourites": "para abrir a lista dos favoritos", + "keyboard_shortcuts.federated": "para abrir a cronologia federada", + "keyboard_shortcuts.heading": "Atalhos do teclado", + "keyboard_shortcuts.home": "para abrir a cronologia inicial", + "keyboard_shortcuts.hotkey": "Atalho", + "keyboard_shortcuts.legend": "para mostrar esta legenda", + "keyboard_shortcuts.local": "para abrir a cronologia local", + "keyboard_shortcuts.mention": "para mencionar o autor", + "keyboard_shortcuts.muted": "para abrir a lista dos utilizadores silenciados", + "keyboard_shortcuts.my_profile": "para abrir o teu perfil", + "keyboard_shortcuts.notifications": "para abrir a coluna das notificações", + "keyboard_shortcuts.pinned": "para abrir a lista dos toots fixados", + "keyboard_shortcuts.profile": "para abrir o perfil do autor", + "keyboard_shortcuts.reply": "para responder", + "keyboard_shortcuts.requests": "para abrir a lista dos pedidos de seguimento", + "keyboard_shortcuts.search": "para focar na pesquisa", + "keyboard_shortcuts.start": "para abrir a coluna dos \"primeiros passos\"", + "keyboard_shortcuts.toggle_hidden": "para mostrar/esconder texto atrás de CW", + "keyboard_shortcuts.toggle_sensitivity": "mostrar/ocultar média", + "keyboard_shortcuts.toot": "para compor um novo toot", + "keyboard_shortcuts.unfocus": "para remover o foco da área de texto/pesquisa", + "keyboard_shortcuts.up": "para mover para cima na lista", + "lightbox.close": "Fechar", + "lightbox.next": "Próximo", + "lightbox.previous": "Anterior", + "lightbox.view_context": "Ver contexto", + "lists.account.add": "Adicionar à lista", + "lists.account.remove": "Remover da lista", + "lists.delete": "Remover lista", + "lists.edit": "Editar lista", + "lists.edit.submit": "Mudar o título", + "lists.new.create": "Adicionar lista", + "lists.new.title_placeholder": "Título da nova lista", + "lists.search": "Pesquisa entre as pessoas que segues", + "lists.subheading": "As tuas listas", + "load_pending": "{count, plural, one {# new item} other {# new items}}", + "loading_indicator.label": "A carregar...", + "media_gallery.toggle_visible": "Mostrar/ocultar", + "missing_indicator.label": "Não encontrado", + "missing_indicator.sublabel": "Este recurso não foi encontrado", + "mute_modal.hide_notifications": "Esconder notificações deste utilizador?", + "navigation_bar.apps": "Aplicações móveis", + "navigation_bar.blocks": "Utilizadores bloqueados", + "navigation_bar.community_timeline": "Cronologia local", + "navigation_bar.compose": "Escrever novo toot", + "navigation_bar.direct": "Mensagens directas", + "navigation_bar.discover": "Descobrir", + "navigation_bar.domain_blocks": "Domínios escondidos", + "navigation_bar.edit_profile": "Editar perfil", + "navigation_bar.favourites": "Favoritos", + "navigation_bar.filters": "Palavras silenciadas", + "navigation_bar.follow_requests": "Seguidores pendentes", + "navigation_bar.follows_and_followers": "Seguindo e seguidores", + "navigation_bar.info": "Sobre este servidor", + "navigation_bar.keyboard_shortcuts": "Atalhos de teclado", + "navigation_bar.lists": "Listas", + "navigation_bar.logout": "Sair", + "navigation_bar.mutes": "Utilizadores silenciados", + "navigation_bar.personal": "Pessoal", + "navigation_bar.pins": "Toots afixados", + "navigation_bar.preferences": "Preferências", + "navigation_bar.public_timeline": "Cronologia federada", + "navigation_bar.security": "Segurança", + "notification.and_n_others": "and {count, plural, one {# other} other {# others}}", + "notification.favourite": "{name} adicionou o teu estado aos favoritos", + "notification.follow": "{name} começou a seguir-te", + "notification.mention": "{name} mencionou-te", + "notification.poll": "Uma votação em participaste chegou ao fim", + "notification.reblog": "{name} fez boost ao teu o teu estado", + "notifications.clear": "Limpar notificações", + "notifications.clear_confirmation": "Queres mesmo limpar todas as notificações?", + "notifications.column_settings.alert": "Notificações no computador", + "notifications.column_settings.favourite": "Favoritos:", + "notifications.column_settings.filter_bar.advanced": "Mostrar todas as categorias", + "notifications.column_settings.filter_bar.category": "Barra de filtros rápidos", + "notifications.column_settings.filter_bar.show": "Mostrar", + "notifications.column_settings.follow": "Novos seguidores:", + "notifications.column_settings.mention": "Menções:", + "notifications.column_settings.poll": "Resultados da votação:", + "notifications.column_settings.push": "Notificações Push", + "notifications.column_settings.reblog": "Boosts:", + "notifications.column_settings.show": "Mostrar na coluna", + "notifications.column_settings.sound": "Reproduzir som", + "notifications.filter.all": "Todas", + "notifications.filter.boosts": "Boosts", + "notifications.filter.favourites": "Favoritos", + "notifications.filter.follows": "Seguimento", + "notifications.filter.mentions": "Referências", + "notifications.filter.polls": "Resultados da votação", + "notifications.group": "{count} notificações", + "poll.closed": "Fechado", + "poll.refresh": "Recarregar", + "poll.total_votes": "{contar, plural, um {# vote} outro {# votes}}", + "poll.vote": "Votar", + "poll_button.add_poll": "Adicionar votação", + "poll_button.remove_poll": "Remover votação", + "privacy.change": "Ajustar a privacidade da mensagem", + "privacy.direct.long": "Apenas para utilizadores mencionados", + "privacy.direct.short": "Directo", + "privacy.private.long": "Apenas para os seguidores", + "privacy.private.short": "Privado", + "privacy.public.long": "Publicar em todos os feeds", + "privacy.public.short": "Público", + "privacy.unlisted.long": "Não publicar nos feeds públicos", + "privacy.unlisted.short": "Não listar", + "regeneration_indicator.label": "A carregar…", + "regeneration_indicator.sublabel": "A tua home está a ser preparada!", + "relative_time.days": "{number}d", + "relative_time.hours": "{number}h", + "relative_time.just_now": "agora", + "relative_time.minutes": "{number}m", + "relative_time.seconds": "{number}s", + "reply_indicator.cancel": "Cancelar", + "report.forward": "Reenviar para {target}", + "report.forward_hint": "A conta é de outro servidor. Enviar uma cópia anónima do relatório para lá também?", + "report.hint": "O relatório será enviado para os moderadores do teu servidor. Podes fornecer, em baixo, uma explicação do motivo pelo qual estás a denunciar esta conta:", + "report.placeholder": "Comentários adicionais", + "report.submit": "Enviar", + "report.target": "Denunciar", + "search.placeholder": "Pesquisar", + "search_popout.search_format": "Formato avançado de pesquisa", + "search_popout.tips.full_text": "Texto simples devolve publicações que tu escreveste, marcaste como favorita, partilhaste ou em que foste mencionado, tal como nomes de utilizador correspondentes, alcunhas e hashtags.", + "search_popout.tips.hashtag": "hashtag", + "search_popout.tips.status": "estado", + "search_popout.tips.text": "O texto simples retorna a correspondência de nomes, utilizadores e hashtags", + "search_popout.tips.user": "utilizador", + "search_results.accounts": "Pessoas", + "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 {resultado} other {resultados}}", + "status.admin_account": "Abrir a interface de moderação para @{name}", + "status.admin_status": "Abrir esta publicação na interface de moderação", + "status.block": "Bloquear @{name}", + "status.cancel_reblog_private": "Remover boost", + "status.cannot_reblog": "Não é possível fazer boost a esta publicação", + "status.copy": "Copiar o link para a publicação", + "status.delete": "Eliminar", + "status.detailed_status": "Vista de conversação detalhada", + "status.direct": "Mensagem directa @{name}", + "status.embed": "Incorporar", + "status.favourite": "Adicionar aos favoritos", + "status.filtered": "Filtrada", + "status.load_more": "Carregar mais", + "status.media_hidden": "Média escondida", + "status.mention": "Mencionar @{name}", + "status.more": "Mais", + "status.mute": "Silenciar @{name}", + "status.mute_conversation": "Silenciar conversa", + "status.open": "Expandir", + "status.pin": "Fixar no perfil", + "status.pinned": "Publicação fixa", + "status.read_more": "Ler mais", + "status.reblog": "Partilhar", + "status.reblog_private": "Fazer boost com a audiência original", + "status.reblogged_by": "{name} fez boost", + "status.reblogs.empty": "Ainda ninguém fez boost a este toot. Quando alguém o fizer, ele irá aparecer aqui.", + "status.redraft": "Apagar & reescrever", + "status.reply": "Responder", + "status.replyAll": "Responder à conversa", + "status.report": "Denunciar @{name}", + "status.sensitive_warning": "Conteúdo sensível", + "status.share": "Partilhar", + "status.show_less": "Mostrar menos", + "status.show_less_all": "Mostrar menos para todas", + "status.show_more": "Mostrar mais", + "status.show_more_all": "Mostrar mais para todas", + "status.show_thread": "Mostrar conversa", + "status.uncached_media_warning": "Not available", + "status.unmute_conversation": "Deixar de silenciar esta conversa", + "status.unpin": "Não fixar no perfil", + "suggestions.dismiss": "Dispensar a sugestão", + "suggestions.header": "Tu podes estar interessado em…", + "tabs_bar.federated_timeline": "Federada", + "tabs_bar.home": "Início", + "tabs_bar.local_timeline": "Local", + "tabs_bar.notifications": "Notificações", + "tabs_bar.search": "Pesquisar", + "time_remaining.days": "{número, plural, um {# day} outro {# days}} faltam", + "time_remaining.hours": "{número, plural, um {# hour} outro {# hours}} faltam", + "time_remaining.minutes": "{número, plural, um {# minute} outro {# minutes}} faltam", + "time_remaining.moments": "Momentos restantes", + "time_remaining.seconds": "{número, plural, um {# second} outro {# seconds}} faltam", + "trends.count_by_accounts": "{count} {rawCount, plural, uma {person} outra {people}} a falar", + "trends.trending_now": "Trending now", + "ui.beforeunload": "O teu rascunho será perdido se abandonares o Mastodon.", + "upload_area.title": "Arraste e solte para enviar", + "upload_button.label": "Adicionar media", + "upload_error.limit": "Limite máximo do ficheiro a carregar excedido.", + "upload_error.poll": "Carregamento de ficheiros não é permitido em votações.", + "upload_form.description": "Descrição da imagem para pessoas com dificuldades visuais", + "upload_form.edit": "Edit", + "upload_form.undo": "Apagar", + "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": "A enviar...", + "video.close": "Fechar vídeo", + "video.exit_fullscreen": "Sair de full screen", + "video.expand": "Expandir vídeo", + "video.fullscreen": "Ecrã completo", + "video.hide": "Esconder vídeo", + "video.mute": "Silenciar", + "video.pause": "Pausar", + "video.play": "Reproduzir", + "video.unmute": "Remover de silêncio" +} diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json deleted file mode 100644 index 63a078c4e..000000000 --- a/app/javascript/mastodon/locales/pt.json +++ /dev/null @@ -1,413 +0,0 @@ -{ - "account.add_or_remove_from_list": "Adicionar ou remover das listas", - "account.badges.bot": "Robô", - "account.block": "Bloquear @{name}", - "account.block_domain": "Esconder tudo do domínio {domain}", - "account.blocked": "Bloqueado", - "account.cancel_follow_request": "Cancel follow request", - "account.direct": "Mensagem directa @{name}", - "account.domain_blocked": "Domínio escondido", - "account.edit_profile": "Editar perfil", - "account.endorse": "Atributo no perfil", - "account.follow": "Seguir", - "account.followers": "Seguidores", - "account.followers.empty": "Ainda ninguém segue este utilizador.", - "account.follows": "Segue", - "account.follows.empty": "Este utilizador ainda não segue alguém.", - "account.follows_you": "É teu seguidor", - "account.hide_reblogs": "Esconder partilhas de @{name}", - "account.last_status": "Last active", - "account.link_verified_on": "A posse deste link foi verificada em {date}", - "account.locked_info": "O estatuto de privacidade desta conta é fechado. O dono revê manualmente que a pode seguir.", - "account.media": "Média", - "account.mention": "Mencionar @{name}", - "account.moved_to": "{name} mudou a sua conta para:", - "account.mute": "Silenciar @{name}", - "account.mute_notifications": "Silenciar notificações de @{name}", - "account.muted": "Silenciada", - "account.never_active": "Never", - "account.posts": "Publicações", - "account.posts_with_replies": "Publicações e respostas", - "account.report": "Denunciar @{name}", - "account.requested": "A aguardar aprovação. Clique para cancelar o pedido de seguimento", - "account.share": "Partilhar o perfil @{name}", - "account.show_reblogs": "Mostrar partilhas de @{name}", - "account.unblock": "Desbloquear @{name}", - "account.unblock_domain": "Mostrar {domain}", - "account.unendorse": "Não mostrar no perfil", - "account.unfollow": "Deixar de seguir", - "account.unmute": "Não silenciar @{name}", - "account.unmute_notifications": "Deixar de silenciar @{name}", - "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", - "alert.rate_limited.title": "Rate limited", - "alert.unexpected.message": "Ocorreu um erro inesperado.", - "alert.unexpected.title": "Bolas!", - "autosuggest_hashtag.per_week": "{count} per week", - "boost_modal.combo": "Pode clicar {combo} para não voltar a ver", - "bundle_column_error.body": "Algo de errado aconteceu enquanto este componente era carregado.", - "bundle_column_error.retry": "Tente de novo", - "bundle_column_error.title": "Erro de rede", - "bundle_modal_error.close": "Fechar", - "bundle_modal_error.message": "Algo de errado aconteceu enquanto este componente era carregado.", - "bundle_modal_error.retry": "Tente de novo", - "column.blocks": "Utilizadores Bloqueados", - "column.community": "Cronologia local", - "column.direct": "Mensagens directas", - "column.directory": "Browse profiles", - "column.domain_blocks": "Domínios escondidos", - "column.favourites": "Favoritos", - "column.follow_requests": "Seguidores pendentes", - "column.home": "Início", - "column.lists": "Listas", - "column.mutes": "Utilizadores silenciados", - "column.notifications": "Notificações", - "column.pins": "Publicações fixas", - "column.public": "Cronologia federada", - "column_back_button.label": "Voltar", - "column_header.hide_settings": "Esconder configurações", - "column_header.moveLeft_settings": "Mover coluna para a esquerda", - "column_header.moveRight_settings": "Mover coluna para a direita", - "column_header.pin": "Fixar", - "column_header.show_settings": "Mostrar configurações", - "column_header.unpin": "Desafixar", - "column_subheading.settings": "Configurações", - "community.column_settings.media_only": "Somente multimédia", - "compose_form.direct_message_warning": "Esta publicação será enviada apenas para os utilizadores mencionados.", - "compose_form.direct_message_warning_learn_more": "Conhecer mais", - "compose_form.hashtag_warning": "Este toot não será listado em nenhuma hashtag por ser não listado. Apenas toots públics podem ser pesquisados por hashtag.", - "compose_form.lock_disclaimer": "A tua conta não está {locked}. Qualquer pessoa pode seguir-te e ver as publicações direcionadas apenas a seguidores.", - "compose_form.lock_disclaimer.lock": "bloqueado", - "compose_form.placeholder": "Em que estás a pensar?", - "compose_form.poll.add_option": "Adicionar uma opção", - "compose_form.poll.duration": "Duração da votação", - "compose_form.poll.option_placeholder": "Opção {number}", - "compose_form.poll.remove_option": "Eliminar esta opção", - "compose_form.publish": "Toot", - "compose_form.publish_loud": "{publish}!", - "compose_form.sensitive.hide": "Marcar multimédia como sensível", - "compose_form.sensitive.marked": "Média marcada como sensível", - "compose_form.sensitive.unmarked": "Média não está marcada como sensível", - "compose_form.spoiler.marked": "Texto escondido atrás de aviso", - "compose_form.spoiler.unmarked": "O texto não está escondido", - "compose_form.spoiler_placeholder": "Escreve o teu aviso aqui", - "confirmation_modal.cancel": "Cancelar", - "confirmations.block.block_and_report": "Bloquear e denunciar", - "confirmations.block.confirm": "Bloquear", - "confirmations.block.message": "De certeza que queres bloquear {name}?", - "confirmations.delete.confirm": "Eliminar", - "confirmations.delete.message": "De certeza que queres eliminar esta publicação?", - "confirmations.delete_list.confirm": "Eliminar", - "confirmations.delete_list.message": "Tens a certeza de que desejas eliminar permanentemente esta lista?", - "confirmations.domain_block.confirm": "Esconder tudo deste domínio", - "confirmations.domain_block.message": "De certeza que queres bloquear completamente o domínio {domain}? Na maioria dos casos, silenciar ou bloquear alguns utilizadores é suficiente e é o recomendado. Não irás ver conteúdo daquele domínio em cronologia alguma nem nas tuas notificações. Os teus seguidores daquele domínio serão removidos.", - "confirmations.logout.confirm": "Log out", - "confirmations.logout.message": "Are you sure you want to log out?", - "confirmations.mute.confirm": "Silenciar", - "confirmations.mute.message": "De certeza que queres silenciar {name}?", - "confirmations.redraft.confirm": "Apagar & redigir", - "confirmations.redraft.message": "Tens a certeza que queres apagar e redigir esta publicação? Os favoritos e as partilhas perder-se-ão e as respostas à publicação original ficarão órfãs.", - "confirmations.reply.confirm": "Responder", - "confirmations.reply.message": "Responder agora irá reescrever a mensagem que estás a compor actualmente. Tens a certeza que queres continuar?", - "confirmations.unfollow.confirm": "Deixar de seguir", - "confirmations.unfollow.message": "De certeza que queres deixar de seguir {name}?", - "directory.federated": "From known fediverse", - "directory.local": "From {domain} only", - "directory.new_arrivals": "New arrivals", - "directory.recently_active": "Recently active", - "embed.instructions": "Publica esta publicação no teu site copiando o código abaixo.", - "embed.preview": "Podes ver aqui como irá ficar:", - "emoji_button.activity": "Actividade", - "emoji_button.custom": "Personalizar", - "emoji_button.flags": "Bandeiras", - "emoji_button.food": "Comida & Bebida", - "emoji_button.label": "Inserir Emoji", - "emoji_button.nature": "Natureza", - "emoji_button.not_found": "Não tem emojis!! (╯°□°)╯︵ ┻━┻", - "emoji_button.objects": "Objectos", - "emoji_button.people": "Pessoas", - "emoji_button.recent": "Utilizados regularmente", - "emoji_button.search": "Pesquisar...", - "emoji_button.search_results": "Resultados da pesquisa", - "emoji_button.symbols": "Símbolos", - "emoji_button.travel": "Viagens & Lugares", - "empty_column.account_timeline": "Sem toots por aqui!", - "empty_column.account_unavailable": "Perfil indisponível", - "empty_column.blocks": "Ainda não bloqueaste qualquer utilizador.", - "empty_column.community": "A timeline local está vazia. Escreve algo publicamente para começar!", - "empty_column.direct": "Ainda não tens qualquer mensagem directa. Quando enviares ou receberes alguma, ela irá aparecer aqui.", - "empty_column.domain_blocks": "Ainda não há qualquer domínio escondido.", - "empty_column.favourited_statuses": "Ainda não tens quaisquer toots favoritos. Quando tiveres algum, ele irá aparecer aqui.", - "empty_column.favourites": "Ainda ninguém marcou este toot como favorito. Quando alguém o fizer, ele irá aparecer aqui.", - "empty_column.follow_requests": "Ainda não tens nenhum pedido de seguimento. Quando receberes algum, ele irá aparecer aqui.", - "empty_column.hashtag": "Não foram encontradas publicações com essa hashtag.", - "empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.", - "empty_column.home.public_timeline": "Cronologia pública", - "empty_column.list": "Ainda não existem publicações nesta lista. Quando membros desta lista fizerem novas publicações, elas aparecerão aqui.", - "empty_column.lists": "Ainda não tens qualquer lista. Quando criares uma, ela irá aparecer aqui.", - "empty_column.mutes": "Ainda não silenciaste qualquer utilizador.", - "empty_column.notifications": "Não tens notificações. Interage com outros utilizadores para iniciar uma conversa.", - "empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para veres aqui os conteúdos públicos", - "follow_request.authorize": "Autorizar", - "follow_request.reject": "Rejeitar", - "getting_started.developers": "Responsáveis pelo desenvolvimento", - "getting_started.directory": "Directório de perfil", - "getting_started.documentation": "Documentação", - "getting_started.heading": "Primeiros passos", - "getting_started.invite": "Convidar pessoas", - "getting_started.open_source_notice": "Mastodon é software de código aberto (open source). Podes contribuir ou reportar problemas no GitHub do projecto: {github}.", - "getting_started.security": "Segurança", - "getting_started.terms": "Termos de serviço", - "hashtag.column_header.tag_mode.all": "e {additional}", - "hashtag.column_header.tag_mode.any": "ou {additional}", - "hashtag.column_header.tag_mode.none": "sem {additional}", - "hashtag.column_settings.select.no_options_message": "Não foram encontradas sugestões", - "hashtag.column_settings.select.placeholder": "Introduzir as hashtags…", - "hashtag.column_settings.tag_mode.all": "Todos estes", - "hashtag.column_settings.tag_mode.any": "Qualquer destes", - "hashtag.column_settings.tag_mode.none": "Nenhum destes", - "hashtag.column_settings.tag_toggle": "Incluir etiquetas adicionais para esta coluna", - "home.column_settings.basic": "Básico", - "home.column_settings.show_reblogs": "Mostrar boosts", - "home.column_settings.show_replies": "Mostrar respostas", - "home.column_settings.update_live": "Update in real-time", - "intervals.full.days": "{number, plural, one {# dia} other {# dias}}", - "intervals.full.hours": "{number, plural, one {# hora} other {# horas}}", - "intervals.full.minutes": "{number, plural, one {# minuto} other {# minutos}}", - "introduction.federation.action": "Seguinte", - "introduction.federation.federated.headline": "Federada", - "introduction.federation.federated.text": "Publicações públicas de outros servidores do fediverse aparecerão na cronologia federativa.", - "introduction.federation.home.headline": "Início", - "introduction.federation.home.text": "As publicações das pessoas que tu segues aparecerão na tua coluna inicial. Tu podes seguir qualquer pessoa em qualquer servidor!", - "introduction.federation.local.headline": "Local", - "introduction.federation.local.text": "Publicações públicas de pessoas que tu segues no teu servidor aparecerão na coluna local.", - "introduction.interactions.action": "Terminar o tutorial!", - "introduction.interactions.favourite.headline": "Favorito", - "introduction.interactions.favourite.text": "Podes guardar um toot para depois e deixar o autor saber que gostaste dele, marcando-o como favorito.", - "introduction.interactions.reblog.headline": "Boost", - "introduction.interactions.reblog.text": "Podes partilhar os toots de outras pessoas com os teus seguidores partilhando-os.", - "introduction.interactions.reply.headline": "Responder", - "introduction.interactions.reply.text": "Tu podes responder a toots de outras pessoas e aos teus, o que os irá juntar numa conversa.", - "introduction.welcome.action": "Vamos!", - "introduction.welcome.headline": "Primeiros passos", - "introduction.welcome.text": "Bem-vindo ao fediverso! Em pouco tempo poderás enviar mensagens e falar com os teus amigos numa grande variedade de servidores. Mas este servidor, {domain}, é especial—ele alberga o teu perfil. Por isso, lembra-te do seu nome.", - "keyboard_shortcuts.back": "para voltar", - "keyboard_shortcuts.blocked": "para abrir a lista de utilizadores bloqueados", - "keyboard_shortcuts.boost": "para partilhar", - "keyboard_shortcuts.column": "para focar uma publicação numa das colunas", - "keyboard_shortcuts.compose": "para focar na área de publicação", - "keyboard_shortcuts.description": "Descrição", - "keyboard_shortcuts.direct": "para abrir a coluna das mensagens directas", - "keyboard_shortcuts.down": "para mover para baixo na lista", - "keyboard_shortcuts.enter": "para expandir um estado", - "keyboard_shortcuts.favourite": "para adicionar aos favoritos", - "keyboard_shortcuts.favourites": "para abrir a lista dos favoritos", - "keyboard_shortcuts.federated": "para abrir a cronologia federada", - "keyboard_shortcuts.heading": "Atalhos do teclado", - "keyboard_shortcuts.home": "para abrir a cronologia inicial", - "keyboard_shortcuts.hotkey": "Atalho", - "keyboard_shortcuts.legend": "para mostrar esta legenda", - "keyboard_shortcuts.local": "para abrir a cronologia local", - "keyboard_shortcuts.mention": "para mencionar o autor", - "keyboard_shortcuts.muted": "para abrir a lista dos utilizadores silenciados", - "keyboard_shortcuts.my_profile": "para abrir o teu perfil", - "keyboard_shortcuts.notifications": "para abrir a coluna das notificações", - "keyboard_shortcuts.pinned": "para abrir a lista dos toots fixados", - "keyboard_shortcuts.profile": "para abrir o perfil do autor", - "keyboard_shortcuts.reply": "para responder", - "keyboard_shortcuts.requests": "para abrir a lista dos pedidos de seguimento", - "keyboard_shortcuts.search": "para focar na pesquisa", - "keyboard_shortcuts.start": "para abrir a coluna dos \"primeiros passos\"", - "keyboard_shortcuts.toggle_hidden": "para mostrar/esconder texto atrás de CW", - "keyboard_shortcuts.toggle_sensitivity": "mostrar/ocultar média", - "keyboard_shortcuts.toot": "para compor um novo toot", - "keyboard_shortcuts.unfocus": "para remover o foco da área de texto/pesquisa", - "keyboard_shortcuts.up": "para mover para cima na lista", - "lightbox.close": "Fechar", - "lightbox.next": "Próximo", - "lightbox.previous": "Anterior", - "lightbox.view_context": "Ver contexto", - "lists.account.add": "Adicionar à lista", - "lists.account.remove": "Remover da lista", - "lists.delete": "Remover lista", - "lists.edit": "Editar lista", - "lists.edit.submit": "Mudar o título", - "lists.new.create": "Adicionar lista", - "lists.new.title_placeholder": "Título da nova lista", - "lists.search": "Pesquisa entre as pessoas que segues", - "lists.subheading": "As tuas listas", - "load_pending": "{count, plural, one {# new item} other {# new items}}", - "loading_indicator.label": "A carregar...", - "media_gallery.toggle_visible": "Mostrar/ocultar", - "missing_indicator.label": "Não encontrado", - "missing_indicator.sublabel": "Este recurso não foi encontrado", - "mute_modal.hide_notifications": "Esconder notificações deste utilizador?", - "navigation_bar.apps": "Aplicações móveis", - "navigation_bar.blocks": "Utilizadores bloqueados", - "navigation_bar.community_timeline": "Cronologia local", - "navigation_bar.compose": "Escrever novo toot", - "navigation_bar.direct": "Mensagens directas", - "navigation_bar.discover": "Descobrir", - "navigation_bar.domain_blocks": "Domínios escondidos", - "navigation_bar.edit_profile": "Editar perfil", - "navigation_bar.favourites": "Favoritos", - "navigation_bar.filters": "Palavras silenciadas", - "navigation_bar.follow_requests": "Seguidores pendentes", - "navigation_bar.follows_and_followers": "Seguindo e seguidores", - "navigation_bar.info": "Sobre este servidor", - "navigation_bar.keyboard_shortcuts": "Atalhos de teclado", - "navigation_bar.lists": "Listas", - "navigation_bar.logout": "Sair", - "navigation_bar.mutes": "Utilizadores silenciados", - "navigation_bar.personal": "Pessoal", - "navigation_bar.pins": "Toots afixados", - "navigation_bar.preferences": "Preferências", - "navigation_bar.public_timeline": "Cronologia federada", - "navigation_bar.security": "Segurança", - "notification.and_n_others": "and {count, plural, one {# other} other {# others}}", - "notification.favourite": "{name} adicionou o teu estado aos favoritos", - "notification.follow": "{name} começou a seguir-te", - "notification.mention": "{name} mencionou-te", - "notification.poll": "Uma votação em participaste chegou ao fim", - "notification.reblog": "{name} fez boost ao teu o teu estado", - "notifications.clear": "Limpar notificações", - "notifications.clear_confirmation": "Queres mesmo limpar todas as notificações?", - "notifications.column_settings.alert": "Notificações no computador", - "notifications.column_settings.favourite": "Favoritos:", - "notifications.column_settings.filter_bar.advanced": "Mostrar todas as categorias", - "notifications.column_settings.filter_bar.category": "Barra de filtros rápidos", - "notifications.column_settings.filter_bar.show": "Mostrar", - "notifications.column_settings.follow": "Novos seguidores:", - "notifications.column_settings.mention": "Menções:", - "notifications.column_settings.poll": "Resultados da votação:", - "notifications.column_settings.push": "Notificações Push", - "notifications.column_settings.reblog": "Boosts:", - "notifications.column_settings.show": "Mostrar na coluna", - "notifications.column_settings.sound": "Reproduzir som", - "notifications.filter.all": "Todas", - "notifications.filter.boosts": "Boosts", - "notifications.filter.favourites": "Favoritos", - "notifications.filter.follows": "Seguimento", - "notifications.filter.mentions": "Referências", - "notifications.filter.polls": "Resultados da votação", - "notifications.group": "{count} notificações", - "poll.closed": "Fechado", - "poll.refresh": "Recarregar", - "poll.total_votes": "{contar, plural, um {# vote} outro {# votes}}", - "poll.vote": "Votar", - "poll_button.add_poll": "Adicionar votação", - "poll_button.remove_poll": "Remover votação", - "privacy.change": "Ajustar a privacidade da mensagem", - "privacy.direct.long": "Apenas para utilizadores mencionados", - "privacy.direct.short": "Directo", - "privacy.private.long": "Apenas para os seguidores", - "privacy.private.short": "Privado", - "privacy.public.long": "Publicar em todos os feeds", - "privacy.public.short": "Público", - "privacy.unlisted.long": "Não publicar nos feeds públicos", - "privacy.unlisted.short": "Não listar", - "regeneration_indicator.label": "A carregar…", - "regeneration_indicator.sublabel": "A tua home está a ser preparada!", - "relative_time.days": "{number}d", - "relative_time.hours": "{number}h", - "relative_time.just_now": "agora", - "relative_time.minutes": "{number}m", - "relative_time.seconds": "{number}s", - "reply_indicator.cancel": "Cancelar", - "report.forward": "Reenviar para {target}", - "report.forward_hint": "A conta é de outro servidor. Enviar uma cópia anónima do relatório para lá também?", - "report.hint": "O relatório será enviado para os moderadores do teu servidor. Podes fornecer, em baixo, uma explicação do motivo pelo qual estás a denunciar esta conta:", - "report.placeholder": "Comentários adicionais", - "report.submit": "Enviar", - "report.target": "Denunciar", - "search.placeholder": "Pesquisar", - "search_popout.search_format": "Formato avançado de pesquisa", - "search_popout.tips.full_text": "Texto simples devolve publicações que tu escreveste, marcaste como favorita, partilhaste ou em que foste mencionado, tal como nomes de utilizador correspondentes, alcunhas e hashtags.", - "search_popout.tips.hashtag": "hashtag", - "search_popout.tips.status": "estado", - "search_popout.tips.text": "O texto simples retorna a correspondência de nomes, utilizadores e hashtags", - "search_popout.tips.user": "utilizador", - "search_results.accounts": "Pessoas", - "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 {resultado} other {resultados}}", - "status.admin_account": "Abrir a interface de moderação para @{name}", - "status.admin_status": "Abrir esta publicação na interface de moderação", - "status.block": "Bloquear @{name}", - "status.cancel_reblog_private": "Remover boost", - "status.cannot_reblog": "Não é possível fazer boost a esta publicação", - "status.copy": "Copiar o link para a publicação", - "status.delete": "Eliminar", - "status.detailed_status": "Vista de conversação detalhada", - "status.direct": "Mensagem directa @{name}", - "status.embed": "Incorporar", - "status.favourite": "Adicionar aos favoritos", - "status.filtered": "Filtrada", - "status.load_more": "Carregar mais", - "status.media_hidden": "Média escondida", - "status.mention": "Mencionar @{name}", - "status.more": "Mais", - "status.mute": "Silenciar @{name}", - "status.mute_conversation": "Silenciar conversa", - "status.open": "Expandir", - "status.pin": "Fixar no perfil", - "status.pinned": "Publicação fixa", - "status.read_more": "Ler mais", - "status.reblog": "Partilhar", - "status.reblog_private": "Fazer boost com a audiência original", - "status.reblogged_by": "{name} fez boost", - "status.reblogs.empty": "Ainda ninguém fez boost a este toot. Quando alguém o fizer, ele irá aparecer aqui.", - "status.redraft": "Apagar & reescrever", - "status.reply": "Responder", - "status.replyAll": "Responder à conversa", - "status.report": "Denunciar @{name}", - "status.sensitive_warning": "Conteúdo sensível", - "status.share": "Partilhar", - "status.show_less": "Mostrar menos", - "status.show_less_all": "Mostrar menos para todas", - "status.show_more": "Mostrar mais", - "status.show_more_all": "Mostrar mais para todas", - "status.show_thread": "Mostrar conversa", - "status.uncached_media_warning": "Not available", - "status.unmute_conversation": "Deixar de silenciar esta conversa", - "status.unpin": "Não fixar no perfil", - "suggestions.dismiss": "Dispensar a sugestão", - "suggestions.header": "Tu podes estar interessado em…", - "tabs_bar.federated_timeline": "Federada", - "tabs_bar.home": "Início", - "tabs_bar.local_timeline": "Local", - "tabs_bar.notifications": "Notificações", - "tabs_bar.search": "Pesquisar", - "time_remaining.days": "{número, plural, um {# day} outro {# days}} faltam", - "time_remaining.hours": "{número, plural, um {# hour} outro {# hours}} faltam", - "time_remaining.minutes": "{número, plural, um {# minute} outro {# minutes}} faltam", - "time_remaining.moments": "Momentos restantes", - "time_remaining.seconds": "{número, plural, um {# second} outro {# seconds}} faltam", - "trends.count_by_accounts": "{count} {rawCount, plural, uma {person} outra {people}} a falar", - "trends.trending_now": "Trending now", - "ui.beforeunload": "O teu rascunho será perdido se abandonares o Mastodon.", - "upload_area.title": "Arraste e solte para enviar", - "upload_button.label": "Adicionar media", - "upload_error.limit": "Limite máximo do ficheiro a carregar excedido.", - "upload_error.poll": "Carregamento de ficheiros não é permitido em votações.", - "upload_form.description": "Descrição da imagem para pessoas com dificuldades visuais", - "upload_form.edit": "Edit", - "upload_form.undo": "Apagar", - "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": "A enviar...", - "video.close": "Fechar vídeo", - "video.exit_fullscreen": "Sair de full screen", - "video.expand": "Expandir vídeo", - "video.fullscreen": "Ecrã completo", - "video.hide": "Esconder vídeo", - "video.mute": "Silenciar", - "video.pause": "Pausar", - "video.play": "Reproduzir", - "video.unmute": "Remover de silêncio" -} diff --git a/app/javascript/mastodon/locales/ro.json b/app/javascript/mastodon/locales/ro.json index 27e4addda..038b8ddd4 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 efbaa25a0..69bd5a422 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 312f63301..89a472d89 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 fa5d22fd1..d7d78c41c 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 12f66cafd..0f851051c 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 72ea3490f..fb6a365ce 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 c77927ec1..064934f54 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 6783da15d..f666a4b6e 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 3266102b1..3caf301d0 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 ee7293aa7..5827dbb3a 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 3ff56f947..33eb315f1 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 ec9bd0f8f..2c4d820de 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 605ebdc08..6ccb20fc6 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_br.json b/app/javascript/mastodon/locales/whitelist_br.json new file mode 100644 index 000000000..0d4f101c7 --- /dev/null +++ b/app/javascript/mastodon/locales/whitelist_br.json @@ -0,0 +1,2 @@ +[ +] diff --git a/app/javascript/mastodon/locales/whitelist_nn.json b/app/javascript/mastodon/locales/whitelist_nn.json new file mode 100644 index 000000000..0d4f101c7 --- /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 000000000..0d4f101c7 --- /dev/null +++ b/app/javascript/mastodon/locales/whitelist_pt-PT.json @@ -0,0 +1,2 @@ +[ +] diff --git a/app/javascript/mastodon/locales/whitelist_pt.json b/app/javascript/mastodon/locales/whitelist_pt.json deleted file mode 100644 index 0d4f101c7..000000000 --- a/app/javascript/mastodon/locales/whitelist_pt.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 8ab7046c1..2f0373d93 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 d63a9dd34..0a42aa47f 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 d0b95da8c..82d7b6db5 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/config/application.rb b/config/application.rb index f49deffbb..5fd37120d 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/locales/activerecord.br.yml b/config/locales/activerecord.br.yml new file mode 100644 index 000000000..c7677c850 --- /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 000000000..777f4e600 --- /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 556fcfc4f..000000000 --- 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 82d2485a7..45001e6fc 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 e801d4b51..72b87a6ac 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 000000000..3710084e7 --- /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 a23b6ddf4..bfd7c514d 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 8ecff0d59..e91b1361f 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 46700be56..569eb35d2 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 a1d637f2e..a58ea2534 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 70397c77b..06a68f684 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 0af7be2f4..fb988668a 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 000000000..c7677c850 --- /dev/null +++ b/config/locales/devise.br.yml @@ -0,0 +1 @@ +br: diff --git a/config/locales/devise.nn.yml b/config/locales/devise.nn.yml new file mode 100644 index 000000000..777f4e600 --- /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 9b44bbf00..000000000 --- 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 000000000..c7677c850 --- /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 000000000..777f4e600 --- /dev/null +++ b/config/locales/doorkeeper.nn.yml @@ -0,0 +1 @@ +nn: diff --git a/config/locales/doorkeeper.pt-PT.yml b/config/locales/doorkeeper.pt-PT.yml new file mode 100644 index 000000000..42068e0a0 --- /dev/null +++ b/config/locales/doorkeeper.pt-PT.yml @@ -0,0 +1,118 @@ +--- +pt-PT: + activerecord: + attributes: + doorkeeper/application: + name: Nome da Aplicação + redirect_uri: URL de redirecionamento + scopes: Autorizações + website: Site da Aplicação + errors: + models: + doorkeeper/application: + attributes: + redirect_uri: + fragment_present: não pode conter um fragmento. + invalid_uri: tem de ser um URI válido. + relative_uri: tem de ser um URI absoluto. + secured_uri: tem de ser um HTTPS/SSL URI. + doorkeeper: + applications: + buttons: + authorize: Autorizar + cancel: Cancelar + destroy: Destruir + edit: Editar + submit: Submeter + confirmations: + destroy: Tens a certeza? + edit: + title: Editar aplicação + form: + error: Oops! Verifica que o formulário não tem erros + help: + native_redirect_uri: Usa %{native_redirect_uri} para testes locais + redirect_uri: Utiliza uma linha por URI + scopes: Separa autorizações com espaços. Deixa em branco para usar autorizações predefinidas. + index: + application: Aplicações + callback_url: URL de retorno + delete: Eliminar + name: Nome + new: Nova Aplicação + scopes: Autorizações + show: Mostrar + title: As tuas aplicações + new: + title: Nova aplicação + show: + actions: Ações + application_id: Id de Aplicação + callback_urls: Callback urls + scopes: Autorizações + secret: Segredo + title: 'Aplicação: %{name}' + authorizations: + buttons: + authorize: Autorize + deny: Não autorize + error: + title: Ocorreu um erro + new: + able_to: Vai poder + prompt: Aplicação %{client_name} pede acesso à tua conta + title: Autorização é necessária + show: + title: Copiar o código desta autorização e colar na aplicação. + authorized_applications: + buttons: + revoke: Revogar + confirmations: + revoke: Tens a certeza? + index: + application: Aplicação + created_at: Criada em + scopes: Autorizações + title: As tuas aplicações autorizadas + errors: + messages: + access_denied: O proprietário do recurso ou servidor de autorização negou o pedido. + credential_flow_not_configured: As credenciais da palavra-passe do proprietário do recurso falhou devido a que Doorkeeper.configure.resource_owner_from_credentials não foram configuradas. + invalid_client: Autenticação do cliente falhou por causa de um cliente desconhecido, nenhum cliente de autenticação incluído ou método de autenticação não suportado. + invalid_grant: A concessão de autorização fornecida é inválida, expirou, foi revogada, não corresponde à URI de redirecionamento usada no pedido de autorização ou foi emitida para outro cliente. + invalid_redirect_uri: A URI de redirecionamento incluída não é válida. + invalid_request: A solicitação não possui um parâmetro requerido, inclui um valor não suportado ou tem outro tipo de formato incorreto. + invalid_resource_owner: As credenciais do proprietário do recurso não são válidas ou o proprietário do recurso não pode ser encontrado + invalid_scope: O âmbito solicitado é inválido, desconhecido ou tem um formato incorreto. + invalid_token: + expired: O token de acesso expirou + revoked: O token de acesso foi revogado + unknown: O token de acesso é inválido + resource_owner_authenticator_not_configured: A procura pelo proprietário do recurso falhou porque Doorkeeper.configure.resource_owner_authenticator não foi configurado. + server_error: O servidor de autorização encontrou uma condição inesperada que impediu o cumprimento do pedido . + temporarily_unavailable: O servidor de autorização não é capaz de lidar com o pedido devido a uma sobrecarga ou mantenimento do servidor. + unauthorized_client: O cliente não está autorizado a realizar esta solicitação usando este método. + unsupported_grant_type: O tipo de concessão de autorização não é suportado pelo servidor de autorização. + unsupported_response_type: O servidor de autorização não suporta este tipo de resposta. + flash: + applications: + create: + notice: Aplicação criada. + destroy: + notice: Aplicação eliminada. + update: + notice: Aplicação alterada. + authorized_applications: + destroy: + notice: Aplicação revogada. + layouts: + admin: + nav: + applications: Aplicações + oauth2_provider: Fornecedor OAuth2 + application: + title: Autorização OAuth necessária + scopes: + follow: siga, bloqueie, desbloqueie, e deixa de seguir contas + read: tenha acesso aos dados da tua conta + write: publique por ti diff --git a/config/locales/doorkeeper.pt.yml b/config/locales/doorkeeper.pt.yml deleted file mode 100644 index f21e84d17..000000000 --- a/config/locales/doorkeeper.pt.yml +++ /dev/null @@ -1,118 +0,0 @@ ---- -pt: - activerecord: - attributes: - doorkeeper/application: - name: Nome da Aplicação - redirect_uri: URL de redirecionamento - scopes: Autorizações - website: Site da Aplicação - errors: - models: - doorkeeper/application: - attributes: - redirect_uri: - fragment_present: não pode conter um fragmento. - invalid_uri: tem de ser um URI válido. - relative_uri: tem de ser um URI absoluto. - secured_uri: tem de ser um HTTPS/SSL URI. - doorkeeper: - applications: - buttons: - authorize: Autorizar - cancel: Cancelar - destroy: Destruir - edit: Editar - submit: Submeter - confirmations: - destroy: Tens a certeza? - edit: - title: Editar aplicação - form: - error: Oops! Verifica que o formulário não tem erros - help: - native_redirect_uri: Usa %{native_redirect_uri} para testes locais - redirect_uri: Utiliza uma linha por URI - scopes: Separa autorizações com espaços. Deixa em branco para usar autorizações predefinidas. - index: - application: Aplicações - callback_url: URL de retorno - delete: Eliminar - name: Nome - new: Nova Aplicação - scopes: Autorizações - show: Mostrar - title: As tuas aplicações - new: - title: Nova aplicação - show: - actions: Ações - application_id: Id de Aplicação - callback_urls: Callback urls - scopes: Autorizações - secret: Segredo - title: 'Aplicação: %{name}' - authorizations: - buttons: - authorize: Autorize - deny: Não autorize - error: - title: Ocorreu um erro - new: - able_to: Vai poder - prompt: Aplicação %{client_name} pede acesso à tua conta - title: Autorização é necessária - show: - title: Copiar o código desta autorização e colar na aplicação. - authorized_applications: - buttons: - revoke: Revogar - confirmations: - revoke: Tens a certeza? - index: - application: Aplicação - created_at: Criada em - scopes: Autorizações - title: As tuas aplicações autorizadas - errors: - messages: - access_denied: O proprietário do recurso ou servidor de autorização negou o pedido. - credential_flow_not_configured: As credenciais da palavra-passe do proprietário do recurso falhou devido a que Doorkeeper.configure.resource_owner_from_credentials não foram configuradas. - invalid_client: Autenticação do cliente falhou por causa de um cliente desconhecido, nenhum cliente de autenticação incluído ou método de autenticação não suportado. - invalid_grant: A concessão de autorização fornecida é inválida, expirou, foi revogada, não corresponde à URI de redirecionamento usada no pedido de autorização ou foi emitida para outro cliente. - invalid_redirect_uri: A URI de redirecionamento incluída não é válida. - invalid_request: A solicitação não possui um parâmetro requerido, inclui um valor não suportado ou tem outro tipo de formato incorreto. - invalid_resource_owner: As credenciais do proprietário do recurso não são válidas ou o proprietário do recurso não pode ser encontrado - invalid_scope: O âmbito solicitado é inválido, desconhecido ou tem um formato incorreto. - invalid_token: - expired: O token de acesso expirou - revoked: O token de acesso foi revogado - unknown: O token de acesso é inválido - resource_owner_authenticator_not_configured: A procura pelo proprietário do recurso falhou porque Doorkeeper.configure.resource_owner_authenticator não foi configurado. - server_error: O servidor de autorização encontrou uma condição inesperada que impediu o cumprimento do pedido . - temporarily_unavailable: O servidor de autorização não é capaz de lidar com o pedido devido a uma sobrecarga ou mantenimento do servidor. - unauthorized_client: O cliente não está autorizado a realizar esta solicitação usando este método. - unsupported_grant_type: O tipo de concessão de autorização não é suportado pelo servidor de autorização. - unsupported_response_type: O servidor de autorização não suporta este tipo de resposta. - flash: - applications: - create: - notice: Aplicação criada. - destroy: - notice: Aplicação eliminada. - update: - notice: Aplicação alterada. - authorized_applications: - destroy: - notice: Aplicação revogada. - layouts: - admin: - nav: - applications: Aplicações - oauth2_provider: Fornecedor OAuth2 - application: - title: Autorização OAuth necessária - scopes: - follow: siga, bloqueie, desbloqueie, e deixa de seguir contas - read: tenha acesso aos dados da tua conta - write: publique por ti diff --git a/config/locales/el.yml b/config/locales/el.yml index 43fec340a..acc97d37e 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/eo.yml b/config/locales/eo.yml index 5785f9b20..b5b8656a4 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 184f0da0e..892d82e9c 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 d02eb24ba..7d0771983 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 56271f3c3..2a4d61296 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 0b4d046f3..7f316c784 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 5a3a8ad60..3d8fdce3a 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 58b160751..15d6359b4 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 0c515a2ec..20f535ad5 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 7aa75434c..5d9097d09 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 f62d309df..7b3eede09 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 d4c6058bf..cfaee9a38 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 b9b9b664f..93cc8ec5a 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 736816425..3658b2293 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 1c4170d8a..fc9fa7b80 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 a5dd5cbf5..7aed705cb 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 25e6c6591..9298e0ae0 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 000000000..a1b61d6e7 --- /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 4cf080be9..1d675aef6 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 65e381b3a..2884380b8 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 0671979fe..23d0c8a98 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 af4d117e0..9896f888a 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-PT.yml b/config/locales/pt-PT.yml new file mode 100644 index 000000000..25ee57085 --- /dev/null +++ b/config/locales/pt-PT.yml @@ -0,0 +1,911 @@ +--- +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. + about_this: Sobre esta instância + administered_by: 'Administrado por:' + apps: Aplicações móveis + contact: Contacto + contact_missing: Não configurado + contact_unavailable: n.d. + documentation: Documentação + extended_description_html: | +

Um bom lugar para regras

+

A descrição estendida ainda não foi configurada.

+ generic_description: "%{domain} é um servidor na rede" + hosted_on: Mastodon em %{domain} + learn_more: Saber mais + privacy_policy: Política de privacidade + source_code: Código fonte + status_count_after: + one: publicação + other: publicações + status_count_before: Que fizeram + terms: termos de serviço + user_count_after: + one: utilizador + other: utilizadores + user_count_before: Casa para + what_is_mastodon: O que é o Mastodon? + accounts: + choices_html: 'escolhas de %{name}:' + follow: Seguir + followers: + one: Seguidor + other: Seguidores + following: A seguir + joined: Aderiu %{date} + last_active: última vez activo + link_verified_on: A posse deste link foi verificada em %{date} + moved_html: "%{name} mudou-se para %{new_profile_link}:" + network_hidden: Esta informação não está disponível + nothing_here: Não há nada aqui! + people_followed_by: Pessoas seguidas por %{name} + people_who_follow: Pessoas que seguem %{name} + pin_errors: + following: Tu tens de estar a seguir a pessoa que pretendes apoiar + posts: + one: Publicação + other: Publicações + posts_tab_heading: Publicações + posts_with_replies: Posts e Respostas + reserved_username: Este nome de utilizadores é reservado + roles: + admin: Administrador + bot: Robô + moderator: Moderador + unfollow: Deixar de seguir + admin: + account_actions: + action: Executar acção + title: Executar acção de moderação em %{acct} + account_moderation_notes: + create: Criar + created_msg: Nota de moderação criada com sucesso! + delete: Eliminar + destroyed_msg: Nota de moderação excluída com sucesso! + accounts: + are_you_sure: Tens a certeza? + by_domain: Domínio + change_email: + changed_msg: E-mail da conta alterado com sucesso! + current_email: E-mail actual + label: Alterar e-mail + new_email: Novo e-mail + submit: Alterar e-mail + title: Alterar e-mail para %{username} + confirm: Confirme + confirmed: Confirmado + confirming: Confirmer + deleted: Apagada + demote: Rebaixar + disable: Desativar + disable_two_factor_authentication: Desativar 2FA + disabled: Desativado + display_name: Nome a mostrar + domain: Domínio + edit: Editar + email: E-mail + email_status: Estado do correio electrónico + enable: Ativar + enabled: Ativado + feed_url: URL do Feed + followers: Seguidores + followers_url: URL dos seguidores + follows: A seguir + header: Cabeçalho + inbox_url: URL da caixa de entrada + invited_by: Convidado por + joined: Aderiu + location: + all: Todos + remote: Remoto + title: Local + login_status: Estado de início de sessão + media_attachments: Media anexa + memorialize: Converter em memorial + moderation: + active: Activo + all: Todos + silenced: Silenciados + suspended: Supensos + title: Moderação + moderation_notes: Notas de moderação + most_recent_activity: Actividade mais recente + most_recent_ip: IP mais recente + no_limits_imposed: Sem limites impostos + not_subscribed: Não inscrito + outbox_url: URL da caixa de saída + perform_full_suspension: Fazer suspensão completa + profile_url: URL do perfil + promote: Promover + protocol: Protocolo + public: Público + push_subscription_expires: A Inscrição PuSH expira + redownload: Atualizar avatar + remove_avatar: Remover o avatar + remove_header: Remover o cabeçalho + resend_confirmation: + already_confirmed: Este usuário já está confirmado + send: Reenviar um email de confirmação + success: Email de confirmação enviado com sucesso! + reset: Restaurar + reset_password: Reset palavra-passe + resubscribe: Reinscrever + role: Permissões + roles: + admin: Administrador + moderator: Moderador + staff: Equipa + user: Utilizador + salmon_url: URL Salmon + search: Pesquisar + shared_inbox_url: URL da caixa de entrada compartilhada + show: + created_reports: Relatórios gerados por esta conta + targeted_reports: Relatórios feitos sobre esta conta + silence: Silêncio + silenced: Silenciada + statuses: Status + subscribe: Inscrever-se + suspended: Suspensa + title: Contas + unconfirmed_email: E-mail não confirmado + undo_silenced: Desfazer silenciar + undo_suspension: Desfazer supensão + unsubscribe: Cancelar inscrição + username: Usuário + warn: Aviso + action_logs: + actions: + assigned_to_self_report: "%{name} atribuiu o relatório %{target} a si próprios" + change_email_user: "%{name} alterou o endereço de e-mail do utilizador %{target}" + confirm_user: "%{name} confirmou o endereço de e-mail do utilizador %{target}" + create_account_warning: "%{name} enviou um aviso para %{target}" + create_custom_emoji: "%{name} enviado emoji novo %{target}" + create_domain_block: "%{name} bloqueou o domínio %{target}" + create_email_domain_block: "%{name} adicionou na lista negra o domínio de correio electrónico %{target}" + demote_user: "%{name} rebaixou o utilizador %{target}" + destroy_custom_emoji: "%{name} destruiu o emoji %{target}" + destroy_domain_block: "%{name} desbloqueou o domínio %{target}" + destroy_email_domain_block: "%{name} adicionou na lista branca o domínio de correio electrónico %{target}" + destroy_status: "%{name} removeu o publicação feita por %{target}" + disable_2fa_user: "%{name} desactivou o requerimento de autenticação em dois passos para o utilizador %{target}" + disable_custom_emoji: "%{name} desabilitou o emoji %{target}" + disable_user: "%{name} desativou o acesso para o utilizador %{target}" + enable_custom_emoji: "%{name} habilitou o emoji %{target}" + enable_user: "%{name} ativou o acesso para o utilizador %{target}" + memorialize_account: "%{name} transformou a conta de %{target} em um memorial" + promote_user: "%{name} promoveu o utilizador %{target}" + remove_avatar_user: "%{name} removeu o avatar de %{target}" + reopen_report: "%{name} reabriu o relatório %{target}" + reset_password_user: "%{name} restabeleceu a palavra-passe do utilizador %{target}" + resolve_report: "%{name} recusou o relatório %{target}" + silence_account: "%{name} silenciou a conta de %{target}" + suspend_account: "%{name} suspendeu a conta de %{target}" + unassigned_report: "%{name} não atribuiu o relatório %{target}" + unsilence_account: "%{name} desativou o silêncio de %{target}" + unsuspend_account: "%{name} desativou a suspensão de %{target}" + update_custom_emoji: "%{name} atualizou o emoji %{target}" + update_status: "%{name} atualizou o estado de %{target}" + deleted_status: "(apagou a publicação)" + title: Registo de auditoria + custom_emojis: + by_domain: Domínio + copied_msg: Cópia local do emoji criada com sucesso + copy: Copiar + copy_failed_msg: Não foi possível criar uma cópia local deste emoji + created_msg: Emoji criado com sucesso! + delete: Apagar + destroyed_msg: Emoji destruído com sucesso! + disable: Desativar + disabled_msg: Desativado com sucesso este emoji + enable: Ativar + enabled_msg: Ativado com sucesso este emoji + image_hint: PNG de até 50KB + listed: Listado + new: + title: Adicionar novo emoji customizado + overwrite: Sobrescrever + shortcode: Código de atalho + shortcode_hint: Pelo menos 2 caracteres, apenas caracteres alfanuméricos e underscores + title: Emojis customizados + unlisted: Não listado + update_failed_msg: Não foi possível atualizar esse emoji + updated_msg: Emoji atualizado com sucesso! + upload: Enviar + dashboard: + backlog: trabalhos atrasados + config: Configuração + feature_deletions: Eliminações da conta + feature_invites: Links de convites + feature_profile_directory: Directório de perfil + feature_registrations: Registos + feature_relay: Repetidor da federação + features: Componentes + hidden_service: Federação com serviços escondidos + open_reports: relatórios abertos + recent_users: Utilizadores recentes + search: Pesquisa com texto completo + single_user_mode: Modo de utilizador único + space: Utilização do espaço + title: Painel de controlo + total_users: total de utilizadores + trends: Tendências + week_interactions: interacções desta semana + week_users_active: activo esta semana + week_users_new: utilizadores nesta semana + domain_blocks: + add_new: Adicionar novo + created_msg: Bloqueio do domínio está a ser processado + destroyed_msg: Bloqueio de domínio está a ser removido + domain: Domínio + new: + create: Criar bloqueio + hint: O bloqueio de dominio não vai previnir a criação de entradas na base de dados, mas irá retroativamente e automaticamente aplicar métodos de moderação específica nessas contas. + severity: + desc_html: "Silenciar irá fazer com que os posts dessas contas sejam invisíveis para todos que não a seguem. Supender irá eliminar todo o conteúdo guardado dessa conta, media e informação de perfil.Usa Nenhum se apenas desejas rejeitar arquivos de media." + noop: Nenhum + silence: Silenciar + suspend: Suspender + title: Novo bloqueio de domínio + reject_media: Rejeitar ficheiros de media + reject_media_hint: Remove localmente arquivos armazenados e rejeita fazer guardar novos no futuro. Irrelevante na suspensão + reject_reports: Rejeitar relatórios + reject_reports_hint: Ignorar todos os relatórios vindos deste domínio. Irrelevantes para efectuar suspensões + rejecting_media: a rejeitar ficheiros de media + rejecting_reports: a rejeitar relatórios + severity: + silence: silenciado + suspend: suspenso + show: + affected_accounts: + one: Uma conta na base de dados afectada + other: "%{count} contas na base de dados afectadas" + retroactive: + silence: Não silenciar todas as contas existentes nesse domínio + suspend: Não suspender todas as contas existentes nesse domínio + title: Remover o bloqueio de domínio de %{domain} + undo: Anular + undo: Anular + email_domain_blocks: + add_new: Adicionar novo + created_msg: Bloqueio de domínio de email criado com sucesso + delete: Eliminar + destroyed_msg: Bloqueio de domínio de email excluído com sucesso + domain: Domínio + new: + create: Adicionar domínio + title: Novo bloqueio de domínio de email + title: Bloqueio de Domínio de Email + followers: + back_to_account: Voltar à conta + title: Seguidores de %{acct} + instances: + by_domain: Domínio + delivery_available: Entrega disponível + known_accounts: + one: "%{count} conta conhecida" + other: "%{count} contas conhecidas" + moderation: + all: Todas + limited: Limitadas + title: Moderação + title: Instâncias conhecidas + total_blocked_by_us: Bloqueadas por nós + total_followed_by_them: Seguidas por eles + total_followed_by_us: Seguidas por nós + total_reported: Relatórios sobre eles + total_storage: Anexos de media + invites: + deactivate_all: Desactivar todos + filter: + all: Todos + available: Disponíveis + expired: Expirados + title: Filtro + title: Convites + relays: + add_new: Adicionar novo repetidor + delete: Apagar + description_html: Um repetidor da federação é um servidor intermediário que troca grandes volumes de publicações públicas entre servidores que o subscrevem e publicam. Ele pode ajudar pequenos e médios servidores a descobrir conteúdo do "fediverse"que, de outro modo, exigiria que os utilizadores locais seguissem manualmente outras pessoas em servidores remotos. + disable: Desactivar + disabled: Desactivado + enable: Activar + enable_hint: Uma vez activado, o teu servidor irá subscrever a todas as publicações deste repetidor e irá começar a enviar as suas publicações públicas para ele. + enabled: Ativado + inbox_url: URL do repetidor + pending: À espera da aprovação do repetidor + save_and_enable: Guardar e ativar + setup: Configurar uma ligação ao repetidor + status: Estado + title: Retransmissores + report_notes: + created_msg: Relatório criado com sucesso! + destroyed_msg: Relatório apagado com sucesso! + reports: + account: + note: nota + report: relatório + action_taken_by: Ação tomada por + are_you_sure: Tens a certeza? + assign_to_self: Atribuí-me a mim + assigned: Atribuído ao moderador + comment: + none: Nenhum + created_at: Relatado + mark_as_resolved: Marcar como resolvido + mark_as_unresolved: Marcar como não resolvido + notes: + create: Adicionar nota + create_and_resolve: Resolver com nota + create_and_unresolve: Reabrir com nota + delete: Apagar + placeholder: Descreve as ações que foram tomadas ou quaisquer outras atualizações relacionadas... + reopen: Reabrir relatório + report: 'Denúncia #%{id}' + reported_account: Conta denunciada + reported_by: Denúnciada por + resolved: Resolvido + resolved_msg: Relatório resolvido com sucesso! + status: Estado + title: Denúncias + unassign: Não atribuir + unresolved: Por resolver + updated_at: Atualizado + settings: + activity_api_enabled: + desc_html: Contagem semanais de publicações locais, utilizadores activos e novos registos + title: Publicar estatísticas agregadas sobre atividade dos utilizadores + bootstrap_timeline_accounts: + desc_html: Separa os nomes de utilizadores por vírgulas. Funciona apenas com contas locais e desbloqueadas. O padrão quando vazio são todos os administradores locais. + title: Seguidores predefinidos para novas contas + contact_information: + email: Inserir um endereço de email para tornar público + username: Insira um nome de utilizador + custom_css: + desc_html: Modificar a aparência com CSS carregado em cada página + title: CSS personalizado + hero: + desc_html: Apresentado na primeira página. Pelo menos 600x100px recomendados. Quando não é definido, é apresentado o thumbnail do servidor + title: Imagem Hero + mascot: + desc_html: Apresentada em múltiplas páginas. Pelo menos 293x205px recomendados. Quando não é definida, é apresentada a mascote predefinida + title: Imagem da mascote + peers_api_enabled: + desc_html: Nomes de domínio que esta instância encontrou no fediverso + title: Publicar lista de instâncias descobertas + preview_sensitive_media: + desc_html: Previsualização de links noutros websites irá apresentar uma miniatura, mesmo que a media seja marcada como sensível + title: Mostrar media sensível em previsualizações OpenGraph + profile_directory: + desc_html: Permite aos utilizadores serem descobertos + title: Ativar directório do perfil + registrations: + closed_message: + desc_html: Mostrar na página inicial quando registos estão encerrados
Podes usar tags HTML + title: Mensagem de registos encerrados + deletion: + desc_html: Permite a qualquer um apagar a conta + title: Permitir eliminar contas + min_invite_role: + disabled: Ninguém + title: Permitir convites de + show_known_fediverse_at_about_page: + desc_html: Quando comutado, irá mostrar a previsualização de publicações de todo o fediverse conhecido. De outro modo só mostrará publicações locais. + title: Mostrar o fediverse conhecido na previsualização da cronologia + show_staff_badge: + desc_html: Mostrar um crachá da equipa na página de utilizador + title: Mostrar crachá da equipa + site_description: + desc_html: Mostrar como parágrafo na página inicial e usado como meta tag.Podes usar tags HTML, em particular <a> e <em>. + title: Descrição do site + site_description_extended: + desc_html: Mostrar na página de mais informações
Podes usar tags HTML + title: Página de mais informações + site_short_description: + desc_html: Mostrada na barra lateral e em etiquetas de metadados. Descreve o que o Mastodon é e o que torna este servidor especial num único parágrafo. Se deixada em branco, remete para a descrição do servidor. + title: Breve descrição do servidor + site_terms: + desc_html: Podes escrever a tua própria política de privacidade, termos de serviço, entre outras coisas. Podes usar tags HTML + title: Termos de serviço customizados + site_title: Título do site + thumbnail: + desc_html: Usada para visualizações via OpenGraph e API. Recomenda-se 1200x630px + title: Miniatura da instância + timeline_preview: + desc_html: Exibir a linha temporal pública na página inicial + title: Visualização da linha temporal + title: Preferências do site + statuses: + back_to_account: Voltar para página da conta + batch: + delete: Eliminar + nsfw_off: NSFW OFF + nsfw_on: NSFW ON + failed_to_execute: Falhou ao executar + media: + title: Média + no_media: Não há média + no_status_selected: Nenhum estado foi alterado porque nenhum foi selecionado + title: Estado das contas + with_media: Com media + title: Administração + warning_presets: + add_new: Adicionar novo + delete: Apagar + edit: Editar + edit_preset: Editar o aviso predefinido + title: Gerir os avisos predefinidos + admin_mailer: + new_report: + body: "%{reporter} relatou %{target}" + body_remote: Alguém de %{domain} relatou %{target} + subject: Novo relatório sobre %{instance} (#%{id}) + application_mailer: + notification_preferences: Alterar preferências de e-mail + settings: 'Alterar preferências de email: %{link}' + view: 'Ver:' + view_profile: Ver perfil + view_status: Ver publicação + applications: + created: Aplicação criada com sucesso + destroyed: Aplicação eliminada com sucesso + invalid_url: O URL é inválido + regenerate_token: Regenerar token de acesso + token_regenerated: Token de acesso regenerado com sucesso + warning: Cuidado com estes dados. Não partilhar com ninguém! + your_token: O teu token de acesso + auth: + change_password: Palavra-passe + delete_account: Eliminar conta + delete_account_html: Se desejas eliminar a conta, podes continua aqui. Uma confirmação será pedida. + didnt_get_confirmation: Não recebeu o email de confirmação? + forgot_password: Esqueceste a palavra-passe? + invalid_reset_password_token: Token de modificação da palavra-passe é inválido ou expirou. Por favor, solicita um novo. + login: Entrar + logout: Sair + migrate_account: Mudar para uma conta diferente + migrate_account_html: Se desejas redirecionar esta conta para uma outra podesconfigurar isso aqui. + or_log_in_with: Ou iniciar sessão com + register: Registar + resend_confirmation: Reenviar instruções de confirmação + reset_password: Criar nova palavra-passe + security: Alterar palavra-passe + set_new_password: Editar palavra-passe + authorize_follow: + already_following: Tu já estás a seguir esta conta + error: Infelizmente, ocorreu um erro ao buscar a conta remota + follow: Seguir + follow_request: 'Enviaste uma solicitação de seguidor para:' + following: 'Sucesso! Agora estás a seguir a:' + post_follow: + close: Ou podes simplesmente fechar esta janela. + return: Voltar ao perfil do utilizador + web: Voltar à página inicial + title: Seguir %{acct} + datetime: + distance_in_words: + about_x_months: "%{count} meses" + about_x_years: "%{count} anos" + almost_x_years: "%{count} anos" + half_a_minute: Justo agora + less_than_x_minutes: "%{count} meses" + less_than_x_seconds: Justo agora + over_x_years: "%{count} anos" + x_days: "%{count} dias" + x_minutes: "%{count} minutos" + x_months: "%{count} meses" + x_seconds: "%{count} segundos" + deletes: + confirm_password: Introduz a palavra-passe atual para verificar a tua identidade + proceed: Eliminar conta + success_msg: A tua conta foi eliminada com sucesso + directories: + directory: Dirétorio de perfil + explanation: Descobre utilizadores com base nos seus interesses + explore_mastodon: Explorar %{title} + errors: + '400': The request you submitted was invalid or malformed. + '403': Não tens a permissão necessária para ver esta página. + '404': A página que estás a procurar não existe. + '406': This page is not available in the requested format. + '410': A página que estás a procurar não existe mais. + '422': + content: "A verificação de segurança falhou. \nDesativaste o uso de cookies?" + title: A verificação de segurança falhou + '429': Desacelerado + '500': + content: Desculpe, mas algo correu mal. + title: Esta página não está correta + '503': The page could not be served due to a temporary server failure. + noscript_html: Para usar o aplicativo web do Mastodon, por favor ativa o JavaScript. Alternativamente, experimenta um dos apps nativos para o Mastodon na sua plataforma. + exports: + archive_takeout: + date: Data + download: Descarregar o teu arquivo + hint_html: Podes pedir um arquivo das tuas publicações e ficheiros de media carregados. Os dados do ficheiro exportado estarão no formato ActivityPub, que pode ser lido com qualquer software compatível. Tu podes pedir um arquivo destes a cada 7 dias. + in_progress: A compilar o seu arquivo... + request: Pede o teu arquivo + size: Tamanho + blocks: Bloqueaste + domain_blocks: Bloqueios de domínio + follows: Segues + lists: Listas + mutes: Tens em silêncio + storage: Armazenamento de média + featured_tags: + add_new: Adicionar nova + errors: + limit: Já atingiste o limite máximo de hashtags + filters: + contexts: + home: Cronologia inicial + notifications: Notificações + public: Cronologias públicas + thread: Conversações + edit: + title: Editar filtros + errors: + invalid_context: Inválido ou nenhum contexto fornecido + invalid_irreversible: Filtragem irreversível só funciona no contexto das notificações ou do início + index: + delete: Apagar + title: Filtros + new: + title: Adicionar novo filtro + footer: + developers: Responsáveis pelo desenvolvimento + more: Mais… + resources: Recursos + generic: + changes_saved_msg: Alterações guardadas! + copy: Copiar + save_changes: Guardar alterações + validation_errors: + one: Algo não está correcto. Por favor vê o erro abaixo + other: Algo não está correto. Por favor vê os %{count} erros abaixo + imports: + modes: + merge: Juntar + merge_long: Manter os registos existentes e adicionar novos registos + overwrite: Escrever por cima + overwrite_long: Substituir os registos atuais pelos novos + preface: Podes importar dados que tenhas exportado de outra instância, como a lista de pessoas que segues ou bloqueadas. + success: Os teus dados foram enviados com sucesso e serão processados em breve + types: + blocking: Lista de bloqueio + domain_blocking: Lista de domínios bloqueados + following: Lista de pessoas que estás a seguir + muting: Lista de utilizadores silenciados + upload: Enviar + in_memoriam_html: Em memória. + invites: + delete: Desativar + expired: Expirados + expires_in: + '1800': 30 minutos + '21600': 6 horas + '3600': 1 hora + '43200': 12 horas + '604800': 1 semana + '86400': 1 dia + expires_in_prompt: Nunca + generate: Gerar + invited_by: 'Tu foste convidado por:' + max_uses: + one: 1 uso + other: "%{count} usos" + max_uses_prompt: Sem limite + prompt: Gerar e partilhar ligações com outras pessoas para permitir acesso a essa instância + table: + expires_at: Expira + uses: Usos + title: Convidar pessoas + lists: + errors: + limit: Número máximo de listas alcançado + media_attachments: + validations: + images_and_video: Não é possível anexar um vídeo a uma publicação que já contém imagens + too_many: Não é possível anexar mais de 4 arquivos + migrations: + acct: username@domain da nova conta + currently_redirecting: 'O teu perfil está configurado para redirecionar para:' + proceed: Salvar + updated_msg: As configurações de migração da tua conta foram atualizadas com sucesso! + moderation: + title: Moderação + notification_mailer: + digest: + action: Ver todas as notificações + body: Aqui tens um breve resumo do que perdeste desde o último acesso a %{since} + mention: "%{name} mencionou-te em:" + new_followers_summary: + one: Tens um novo seguidor! Boa! + other: Tens %{count} novos seguidores! Fantástico! + subject: + one: "1 nova notificação desde o último acesso \U0001F418" + other: "%{count} novas notificações desde o último acesso \U0001F418" + title: Enquanto estiveste ausente… + favourite: + body: 'O teu post foi adicionado aos favoritos por %{name}:' + subject: "%{name} adicionou o teu post aos favoritos" + title: Novo favorito + follow: + body: "%{name} é teu seguidor!" + subject: "%{name} começou a seguir-te" + title: Novo seguidor + follow_request: + action: Gerir pedidos de seguidores + body: "%{name} solicitou autorização para te seguir" + subject: 'Seguidor pendente: %{name}' + title: Nova solicitação de seguidor + mention: + action: Responder + body: 'Foste mencionado por %{name}:' + subject: "%{name} mencionou-te" + title: Nova menção + reblog: + body: 'O teu post foi partilhado por %{name}:' + subject: "%{name} partilhou o teu post" + title: Nova partilha + pagination: + newer: Mais nova + next: Seguinte + older: Mais velha + prev: Anterior + polls: + errors: + already_voted: Tu já votaste nesta sondagem + duplicate_options: contém itens duplicados + duration_too_long: está demasiado à frente no futuro + duration_too_short: é demasiado cedo + expired: A sondagem já terminou + over_character_limit: não pode ter mais do que %{max} caracteres cada um + too_few_options: tem de ter mais do que um item + too_many_options: não pode conter mais do que %{max} itens + preferences: + other: Outro + remote_follow: + acct: Entre seu usuário@domínio do qual quer seguir + missing_resource: Não foi possível achar a URL de redirecionamento para sua conta + no_account_html: Não tens uma conta? Tu podes aderir aqui + proceed: Prossiga para seguir + prompt: 'Você vai seguir:' + reason_html: " Porque é que este passo é necessário? %{instance} pode não ser o servidor onde tu estás registado. Por isso, nós precisamos de te redirecionar para o teu servidor inicial em primeiro lugar." + remote_interaction: + favourite: + proceed: Prosseguir para os favoritos + prompt: 'Queres favoritar esta publicação:' + reblog: + proceed: Prosseguir com partilha + prompt: 'Queres partilhar esta publicação:' + reply: + proceed: Prosseguir com resposta + prompt: 'Queres responder a esta publicação:' + scheduled_statuses: + over_daily_limit: Excedeste o limite de %{limit} publicações agendadas para esse dia + over_total_limit: Tu excedeste o limite de %{limit} publicações agendadas + too_soon: A data de agendamento tem de ser futura + sessions: + activity: Última atividade + browser: Navegador + browsers: + generic: Navegador desconhecido + nokia: Navegador Nokia S40 Ovi + otter: Lontra + current_session: Sessão atual + description: "%{browser} em %{platform}" + explanation: Estes são os navegadores que estão conectados com a tua conta do Mastodon. + platforms: + firefox_os: SO Firefox + other: Plataforma desconhecida + revoke: Revogar + revoke_success: Sessão revogada com sucesso + title: Sessões + settings: + authorized_apps: Aplicativos autorizados + back: Voltar ao Mastodon + delete: Eliminação da conta + development: Desenvolvimento + edit_profile: Editar perfil + export: Exportar dados + featured_tags: Hashtags destacadas + import: Importar + migrate: Migração de conta + notifications: Notificações + preferences: Preferências + two_factor_authentication: Autenticação em dois passos + statuses: + attached: + description: 'Anexadas: %{attached}' + image: + one: "%{count} imagem" + other: "%{count} imagens" + video: + one: "%{count} vídeo" + other: "%{count} vídeos" + boosted_from_html: Partilhadas de %{acct_link} + content_warning: 'Aviso de conteúdo: %{warning}' + disallowed_hashtags: + one: 'continha uma hashtag proibida: %{tags}' + other: 'continha as hashtags proibidas: %{tags}' + language_detection: Detectar automaticamente a língua + open_in_web: Abrir no browser + over_character_limit: limite de caracter excedeu %{max} + pin_errors: + limit: Já fixaste a quantidade máxima de publicações + ownership: Posts de outras pessoas não podem ser fixados + private: Post não-público não pode ser fixado + reblog: Não podes fixar uma partilha + poll: + total_votes: + one: "%{count} voto" + other: "%{count} votos" + vote: Votar + show_more: Mostrar mais + sign_in_to_participate: Inicie a sessão para participar na conversa + visibilities: + private: Mostrar apenas para seguidores + private_long: Mostrar apenas para seguidores + public: Público + public_long: Todos podem ver + unlisted: Público, mas não mostre no timeline público + unlisted_long: Todos podem ver, porém não será postado nas timelines públicas + stream_entries: + pinned: Toot fixado + reblogged: partilhado + sensitive_content: Conteúdo sensível + terms: + body_html: | +

Política de privacidade

+

Que informação nós recolhemos?

+ +
    +
  • Informação básica da conta: Se te registares neste servidor, pode-te ser pedido que indiques um nome de utilizador, um endereço de e-mail e uma palavra-passe. Também podes introduzir informação adicional de perfil, tal como um nome a mostrar e dados biográficos, que carregues uma fotografia para o teu perfil e para o cabeçalho. O nome de utilizador, o nome a mostrar, a biografia, a imagem de perfil e a imagem de cabeçalho são sempre listados publicamente.
  • +
  • Publicações, seguimento e outra informação pública: A lista de pessoas que tu segues é pública, o mesmo é verdade para os teus seguidores. Quando tu publicas uma mensagem, a data e a hora são guardados, tal como a aplicação a partir da qual a mensagem foi enviada. As mensagens podem conter anexos multimédia, tais como fotografias ou vídeos. Publicações públicas e não listadas são acessíveis publicamente. Quando expões uma publicação no teu perfil, isso é também informação disponível publicamente. As tuas publicações são enviadas aos teus seguidores. Em alguns casos isso significa que elas são enviadas para servidores diferentes onde são guardadas cópias. Quando tu apagas publicações, isso também é enviado para os teus seguidores. A acção de republicar ou favoritar outra publicação é sempre pública.
  • +
  • Publicações directas e exclusivas para seguidores: Todas as publicações são guardadas e processadas no servidor. Publicações exclusivas para seguidores são enviadas para os teus seguidores e para utilizadores que são nelas mencionados. As publicações directas são enviadas apenas para os utilizadores nelas mencionados. Em alguns casos isso significa que elas são enviadas para diferentes servidores onde são guardadas cópias das mesmas. Nós fazemos um grande esforço para limitar o acesso a estas publicações aos utilizadores autorizados, mas outros servidores podem falhar neste objectivo. Por isso, tu deves rever os servidores a que os teus seguidores pertencem. Tu podes activar uma opção para aprovar e rejeitar manualmente novos seguidores nas configurações. Por favor, tem em mente que os gestores do servidor e qualquer servidor que receba a publicação pode lê-lae que os destinatários podem fazer uma captura de tela, copiar ou partilhar a publicação. Não partilhes qualquer informação perigosa no Mastodon.
  • +
  • IPs e outros metadados: Quando inicias sessão, nós guardamos o endereço de IP a partir do qual iniciaste a sessão, tal como o nome do teu navegador. Todas as sessões estão disponíveis para verificação e revogação nas configurações. O último endereço de IP usado é guardado até 12 meses. Nós também podemos guardar registos de servidor, os quais incluem o endereço de IP de cada pedido dirigido ao nosso servidor.
  • +
+ +
+ +

Para que usamos a tua informação?

+ +

Qualquer informação que recolhemos sobre ti pode ser usada dos seguintes modos:

+ +
    +
  • Para providenciar a funcionalidade central do Mastodon. Tu só podes interagir com o conteúdo de outras pessoas e publicar o teu próprio conteúdo depois de teres iniciado sessão. Por exemplo, tu podes seguir outras pessoas para veres as suas publicações na tua cronologia inicial personalizada.
  • +
  • Para ajudar na moderação da comunidade para, por exemplo, comparar o teu endereço IP com outros conhecidos, para determinar a fuga ao banimento ou outras violações.
  • +
  • O endereço de e-mail que tu forneces pode ser usado para te enviar informações e/ou notificações sobre outras pessoas que estão a interagir com o teu conteúdo ou a enviar-te mensagens, para responderes a inquéritos e/ou outros pedidos ou questões.
  • +
+ +
+ +

Como é que nós protegemos a tua informação?

+ +

Nós implementamos uma variedade de medidas de segurança para garantir a segurança da tua informação pessoal quando tu introduzes, submetes ou acedes à mesma. Entre outras coisas, a tua sessão de navegação, tal como o tráfego entre as tuas aplicações e a API, estão seguras por SSL e a tua palavra-passe é codificada usando um forte algoritmo de sentido único. Tu podes activar a autenticação em dois passos para aumentares ainda mais a segurança do acesso à tua conta.

+ +
+ +

Qual é a nossa política de retenção de dados?

+ +

Nós envidaremos todos os esforços no sentido de:

+ +
    +
  • Guardar registos do servidor contendo o endereço de IP de todos os pedidos feitos a este servidor, considerando que estes registos não serão guardados por mais de 90 dias.
  • +
  • Guardar os endereços de IP associados aos utilizadores registados durante um período que não ultrapassará os 12 meses.
  • +
+ +

Tu podes pedir e descarregar um ficheiro com o teu conteúdo, incluindo as tuas publicações, os ficheiros multimédia, a imagem de perfil e a imagem de cabeçalho.

+ +

Tu podes apagar a tua conta de modo definitivo e a qualquer momento.

+ +
+ +

Usamos cookies?

+ +

Sim. Cookies são pequenos ficheiros que um site ou o seu fornecedor de serviço transfere para o disco rígido do teu computador através do teu navegador (se tu permitires). Estes cookies permitem ao site reconhecer o teu navegador e, se tu tiveres uma conta registada, associá-lo a ela.

+ +

Nós usamos os cookies para compreender e guardar as tuas preferências para as visitas futuras.

+ +
+ +

Nós divulgamos alguma informação para entidades externas?

+ +

Nós não vendemos, trocamos ou transferimos de qualquer modo a tua informação pessoal que seja identificável para qualquer entidade externa. Isto não inclui terceiros de confiança que nos ajudam a manter o nosso site, conduzir o nosso negócio ou prestar-te este serviço, desde que esses terceiros concordem em manter essa informação confidencial. Poderemos também revelar a tua informação quando nós acreditamos que isso é apropriado para cumprir a lei, forçar a aplicação dos nossos termos de serviço ou proteger os direitos, propriedade e segurança, nossos e de outrem.

+ +

O teu conteúdo público pode ser descarregado por outros servidores na rede. As tuas publicações públicas e exclusivas para os teus seguidores são enviadas para os servidores onde os teus seguidores residem e as mensagens directas são entregues aos servidores dos seus destinatários, no caso desses seguidores ou destinatários residirem num servidor diferente deste.

+ +

Quando tu autorizas uma aplicação a usar a tua conta, dependendo da abrangência das permissões que tu aprovas, ela pode ter acesso à informação pública do teu perfil, à lista de quem segues, aos teus seguidores, às tuas listas, a todas as tuas publicações e aos teus favoritos. As aplicações nunca terão acesso ao teu endereço de e-mail ou à tua palavra-passe.

+ +
+ +

Utilização do site por crianças

+ +

Se este servidor estiver na EU ou na EEA: O nosso site, produtos e serviços são todos dirigidos a pessoas que têm, pelo menos, 16 de idade. Se tu tens menos de 16 anos, devido aos requisitos da GDPR (General Data Protection Regulation) não uses este site.

+ +

Se este servidor estiver nos EUA: O nosso site, produtos e serviços são todos dirigidos a pessoas que têm, pelo menos, 13 anos de idade. Se tu tens menos de 13 anos de idade, devido aos requisitos da COPPA (Children's Online Privacy Protection Act) não uses este site.

+ +

Os requisitos legais poderão ser diferentes se este servidor estiver noutra jurisdição.

+ +
+ +

Alterações à nossa Política de Privacidade

+ +

Se nós decidirmos alterar a nossa política de privacidade, nós iremos publicar essas alterações nesta página.

+ +

Este documento é CC-BY-SA. Ele foi actualizado pela última vez em 7 de Março 2018.

+ +

Originalmente adaptado de Discourse privacy policy.

+ title: "%{instance} Termos de Serviço e Política de Privacidade" + themes: + contrast: Mastodon (Elevado contraste) + default: Mastodon + mastodon-light: Mastodon (Leve) + two_factor_authentication: + code_hint: Entre o código gerado pelo seu aplicativo para confirmar + description_html: Se ativar a autenticação em dois passos, quando logar será necessário o seu telefone que vai gerar os tokens para validação. + disable: Desativar + enable: Ativar + enabled: A autenticação em dois passos está ativada + enabled_success: Autenticação em dois passos ativada com sucesso + generate_recovery_codes: Gerar códigos para recuperar conta + instructions_html: "Scaneie este código QR no seu Google Authenticator ou aplicativo similar no seu telefone. A partir de agora seu aplicativo irá gerar tokens que deverão ser digitados para você logar." + lost_recovery_codes: Códigos de recuperação permite que você recupere o acesso a sua conta se você perder seu telefone. Se você perder os códigos de recuperação, você pode regera-los aqui. Seus códigos antigos serão invalidados. + manual_instructions: 'Se você não puder scanear o código QR e precisa digita-los manualmente, aqui está o segredo em texto.:' + recovery_codes: Cópia de segurança dos códigos de recuperação + recovery_codes_regenerated: Códigos de recuperação foram gerados com sucesso + recovery_instructions_html: Se tu alguma vez perderes o teu smartphone, to poderás usar um dos códigos de recuperação para voltares a ter acesso à tua conta. Mantém os códigos de recuperação seguros. Por exemplo, tu podes imprimi-los e guardá-los junto a outros documentos importantes. + setup: Configurar + wrong_code: O código inserido é invalido! O horário do servidor e o horário do seu aparelho estão corretos? + user_mailer: + backup_ready: + explanation: Pediste uma cópia completa da tua conta Mastodon. Ela já está pronta para descarregares! + subject: O teu arquivo está pronto para descarregar + title: Arquivo de ficheiros + warning: + explanation: + disable: Enquanto a tua conta está congelada, os seus dados permanecem intactos, mas tu não podes executar quaisquer acções até que ela seja desbloqueada. + silence: Enquanto a tua conta estiver limitada, só pessoas que já estiveres a seguir irão ver as tuas publicações neste servidor e poderás ser excluído de várias listagens públicas. No entanto, outros ainda te poderão seguir de forma manual. + suspend: A tua conta foi suspensa e todas as tuas publicações e os teus ficheiros de media foram irreversivelmente removidos deste servidor e dos servidores onde tinhas seguidores. + review_server_policies: Revê as políticas do servidor + subject: + disable: A tua conta %{acct} foi congelada + none: Aviso para %{acct} + silence: A tua conta %{acct} foi limitada + suspend: A tua conta %{acct} foi suspensa + title: + disable: Conta congelada + none: Aviso + silence: Conta limitada + suspend: Conta suspensa + welcome: + edit_profile_action: Configura o perfil + edit_profile_step: Tu podes personalizar o teu perfil por carregar um avatar, cabeçalho, alterar o teu nickname e mais. Se tu preferires rever os novos seguidores antes deles te poderem seguir, podes bloquear a tua conta. + explanation: Aqui estão algumas dicas para começares + final_action: Começa a publicar + final_step: 'Começa a publicar! Mesmo sem seguidores, as tuas mensagens públicas podem ser vistas por outros, por exemplo, na cronologia local e em hashtags. Tu podes querer apresentar-te na hashtag #introductions.' + full_handle: O teu nome completo + full_handle_hint: Isto é o que tu dirias aos teus amigos para que eles te possam enviar mensagens ou seguir-te a partir de outro servidor. + review_preferences_action: Alterar preferências + review_preferences_step: Certifica-te de configurar as tuas preferências, tais como os e-mails que gostarias de receber ou o nível de privacidade que desejas que as tuas publicações tenham por defeito. Se não sofres de enjoo, podes activar a opção GIF autoplay. + subject: Bem-vindo ao Mastodon + tip_federated_timeline: A cronologia federativa é uma visão global da rede Mastodon. Mas só inclui pessoas que os teus vizinhos subscrevem, por isso não é uma visão completa. + tip_following: Tu segues o(s) administrador(es) do teu servidor por defeito. Para encontrares mais pessoas interessantes, procura nas cronologias local e federativa. + tip_local_timeline: A cronologia local é uma visão global das pessoas em %{instance}. Estes são os teus vizinhos próximos! + tip_mobile_webapp: Se o teu navegador móvel te oferecer a possibilidade de adicionar o Mastodon ao teu homescreen, tu podes receber notificações push. Ele age como uma aplicação nativa de vários modos! + tips: Dicas + title: Bem-vindo a bordo, %{name}! + users: + follow_limit_reached: Não podes seguir mais do que %{limit} pessoas + invalid_email: O endereço de e-mail é inválido + invalid_otp_token: Código de autenticação inválido + otp_lost_help_html: Se tu perdeste acesso a ambos, tu podes entrar em contacto com %{email} + seamless_external_login: Tu estás ligado via um serviço externo. Por isso, as configurações da palavra-passe e do e-mail não estão disponíveis. + signed_in_as: 'Registado como:' + verification: + explanation_html: 'Tu podes comprovar que és o dono dos links nos metadados do teu perfil. Para isso, o website para o qual o link aponta tem de conter um link para o teu perfil do Mastodon. Este link tem de ter um rel="me" atributo. O conteúdo do texto não é relevante. Aqui está um exemplo:' + verification: Verificação diff --git a/config/locales/pt.yml b/config/locales/pt.yml deleted file mode 100644 index eeb158f6c..000000000 --- a/config/locales/pt.yml +++ /dev/null @@ -1,912 +0,0 @@ ---- -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. - about_this: Sobre esta instância - administered_by: 'Administrado por:' - apps: Aplicações móveis - contact: Contacto - contact_missing: Não configurado - contact_unavailable: n.d. - documentation: Documentação - extended_description_html: | -

Um bom lugar para regras

-

A descrição estendida ainda não foi configurada.

- generic_description: "%{domain} é um servidor na rede" - hosted_on: Mastodon em %{domain} - learn_more: Saber mais - privacy_policy: Política de privacidade - source_code: Código fonte - status_count_after: - one: publicação - other: publicações - status_count_before: Que fizeram - terms: termos de serviço - user_count_after: - one: utilizador - other: utilizadores - user_count_before: Casa para - what_is_mastodon: O que é o Mastodon? - accounts: - choices_html: 'escolhas de %{name}:' - follow: Seguir - followers: - one: Seguidor - other: Seguidores - following: A seguir - joined: Aderiu %{date} - last_active: última vez activo - link_verified_on: A posse deste link foi verificada em %{date} - moved_html: "%{name} mudou-se para %{new_profile_link}:" - network_hidden: Esta informação não está disponível - nothing_here: Não há nada aqui! - people_followed_by: Pessoas seguidas por %{name} - people_who_follow: Pessoas que seguem %{name} - pin_errors: - following: Tu tens de estar a seguir a pessoa que pretendes apoiar - posts: - one: Publicação - other: Publicações - posts_tab_heading: Publicações - posts_with_replies: Posts e Respostas - reserved_username: Este nome de utilizadores é reservado - roles: - admin: Administrador - bot: Robô - moderator: Moderador - unfollow: Deixar de seguir - admin: - account_actions: - action: Executar acção - title: Executar acção de moderação em %{acct} - account_moderation_notes: - create: Criar - created_msg: Nota de moderação criada com sucesso! - delete: Eliminar - destroyed_msg: Nota de moderação excluída com sucesso! - accounts: - are_you_sure: Tens a certeza? - by_domain: Domínio - change_email: - changed_msg: E-mail da conta alterado com sucesso! - current_email: E-mail actual - label: Alterar e-mail - new_email: Novo e-mail - submit: Alterar e-mail - title: Alterar e-mail para %{username} - confirm: Confirme - confirmed: Confirmado - confirming: Confirmer - deleted: Apagada - demote: Rebaixar - disable: Desativar - disable_two_factor_authentication: Desativar 2FA - disabled: Desativado - display_name: Nome a mostrar - domain: Domínio - edit: Editar - email: E-mail - email_status: Estado do correio electrónico - enable: Ativar - enabled: Ativado - feed_url: URL do Feed - followers: Seguidores - followers_url: URL dos seguidores - follows: A seguir - header: Cabeçalho - inbox_url: URL da caixa de entrada - invited_by: Convidado por - joined: Aderiu - location: - all: Todos - remote: Remoto - title: Local - login_status: Estado de início de sessão - media_attachments: Media anexa - memorialize: Converter em memorial - moderation: - active: Activo - all: Todos - silenced: Silenciados - suspended: Supensos - title: Moderação - moderation_notes: Notas de moderação - most_recent_activity: Actividade mais recente - most_recent_ip: IP mais recente - no_limits_imposed: Sem limites impostos - not_subscribed: Não inscrito - outbox_url: URL da caixa de saída - perform_full_suspension: Fazer suspensão completa - profile_url: URL do perfil - promote: Promover - protocol: Protocolo - public: Público - push_subscription_expires: A Inscrição PuSH expira - redownload: Atualizar avatar - remove_avatar: Remover o avatar - remove_header: Remover o cabeçalho - resend_confirmation: - already_confirmed: Este usuário já está confirmado - send: Reenviar um email de confirmação - success: Email de confirmação enviado com sucesso! - reset: Restaurar - reset_password: Reset palavra-passe - resubscribe: Reinscrever - role: Permissões - roles: - admin: Administrador - moderator: Moderador - staff: Equipa - user: Utilizador - salmon_url: URL Salmon - search: Pesquisar - shared_inbox_url: URL da caixa de entrada compartilhada - show: - created_reports: Relatórios gerados por esta conta - targeted_reports: Relatórios feitos sobre esta conta - silence: Silêncio - silenced: Silenciada - statuses: Status - subscribe: Inscrever-se - suspended: Suspensa - title: Contas - unconfirmed_email: E-mail não confirmado - undo_silenced: Desfazer silenciar - undo_suspension: Desfazer supensão - unsubscribe: Cancelar inscrição - username: Usuário - warn: Aviso - action_logs: - actions: - assigned_to_self_report: "%{name} atribuiu o relatório %{target} a si próprios" - change_email_user: "%{name} alterou o endereço de e-mail do utilizador %{target}" - confirm_user: "%{name} confirmou o endereço de e-mail do utilizador %{target}" - create_account_warning: "%{name} enviou um aviso para %{target}" - create_custom_emoji: "%{name} enviado emoji novo %{target}" - create_domain_block: "%{name} bloqueou o domínio %{target}" - create_email_domain_block: "%{name} adicionou na lista negra o domínio de correio electrónico %{target}" - demote_user: "%{name} rebaixou o utilizador %{target}" - destroy_custom_emoji: "%{name} destruiu o emoji %{target}" - destroy_domain_block: "%{name} desbloqueou o domínio %{target}" - destroy_email_domain_block: "%{name} adicionou na lista branca o domínio de correio electrónico %{target}" - destroy_status: "%{name} removeu o publicação feita por %{target}" - disable_2fa_user: "%{name} desactivou o requerimento de autenticação em dois passos para o utilizador %{target}" - disable_custom_emoji: "%{name} desabilitou o emoji %{target}" - disable_user: "%{name} desativou o acesso para o utilizador %{target}" - enable_custom_emoji: "%{name} habilitou o emoji %{target}" - enable_user: "%{name} ativou o acesso para o utilizador %{target}" - memorialize_account: "%{name} transformou a conta de %{target} em um memorial" - promote_user: "%{name} promoveu o utilizador %{target}" - remove_avatar_user: "%{name} removeu o avatar de %{target}" - reopen_report: "%{name} reabriu o relatório %{target}" - reset_password_user: "%{name} restabeleceu a palavra-passe do utilizador %{target}" - resolve_report: "%{name} recusou o relatório %{target}" - silence_account: "%{name} silenciou a conta de %{target}" - suspend_account: "%{name} suspendeu a conta de %{target}" - unassigned_report: "%{name} não atribuiu o relatório %{target}" - unsilence_account: "%{name} desativou o silêncio de %{target}" - unsuspend_account: "%{name} desativou a suspensão de %{target}" - update_custom_emoji: "%{name} atualizou o emoji %{target}" - update_status: "%{name} atualizou o estado de %{target}" - deleted_status: "(apagou a publicação)" - title: Registo de auditoria - custom_emojis: - by_domain: Domínio - copied_msg: Cópia local do emoji criada com sucesso - copy: Copiar - copy_failed_msg: Não foi possível criar uma cópia local deste emoji - created_msg: Emoji criado com sucesso! - delete: Apagar - destroyed_msg: Emoji destruído com sucesso! - disable: Desativar - disabled_msg: Desativado com sucesso este emoji - enable: Ativar - enabled_msg: Ativado com sucesso este emoji - image_hint: PNG de até 50KB - listed: Listado - new: - title: Adicionar novo emoji customizado - overwrite: Sobrescrever - shortcode: Código de atalho - shortcode_hint: Pelo menos 2 caracteres, apenas caracteres alfanuméricos e underscores - title: Emojis customizados - unlisted: Não listado - update_failed_msg: Não foi possível atualizar esse emoji - updated_msg: Emoji atualizado com sucesso! - upload: Enviar - dashboard: - backlog: trabalhos atrasados - config: Configuração - feature_deletions: Eliminações da conta - feature_invites: Links de convites - feature_profile_directory: Directório de perfil - feature_registrations: Registos - feature_relay: Repetidor da federação - features: Componentes - hidden_service: Federação com serviços escondidos - open_reports: relatórios abertos - recent_users: Utilizadores recentes - search: Pesquisa com texto completo - single_user_mode: Modo de utilizador único - space: Utilização do espaço - title: Painel de controlo - total_users: total de utilizadores - trends: Tendências - week_interactions: interacções desta semana - week_users_active: activo esta semana - week_users_new: utilizadores nesta semana - domain_blocks: - add_new: Adicionar novo - created_msg: Bloqueio do domínio está a ser processado - destroyed_msg: Bloqueio de domínio está a ser removido - domain: Domínio - new: - create: Criar bloqueio - hint: O bloqueio de dominio não vai previnir a criação de entradas na base de dados, mas irá retroativamente e automaticamente aplicar métodos de moderação específica nessas contas. - severity: - desc_html: "Silenciar irá fazer com que os posts dessas contas sejam invisíveis para todos que não a seguem. Supender irá eliminar todo o conteúdo guardado dessa conta, media e informação de perfil.Usa Nenhum se apenas desejas rejeitar arquivos de media." - noop: Nenhum - silence: Silenciar - suspend: Suspender - title: Novo bloqueio de domínio - reject_media: Rejeitar ficheiros de media - reject_media_hint: Remove localmente arquivos armazenados e rejeita fazer guardar novos no futuro. Irrelevante na suspensão - reject_reports: Rejeitar relatórios - reject_reports_hint: Ignorar todos os relatórios vindos deste domínio. Irrelevantes para efectuar suspensões - rejecting_media: a rejeitar ficheiros de media - rejecting_reports: a rejeitar relatórios - severity: - silence: silenciado - suspend: suspenso - show: - affected_accounts: - one: Uma conta na base de dados afectada - other: "%{count} contas na base de dados afectadas" - retroactive: - silence: Não silenciar todas as contas existentes nesse domínio - suspend: Não suspender todas as contas existentes nesse domínio - title: Remover o bloqueio de domínio de %{domain} - undo: Anular - undo: Anular - email_domain_blocks: - add_new: Adicionar novo - created_msg: Bloqueio de domínio de email criado com sucesso - delete: Eliminar - destroyed_msg: Bloqueio de domínio de email excluído com sucesso - domain: Domínio - new: - create: Adicionar domínio - title: Novo bloqueio de domínio de email - title: Bloqueio de Domínio de Email - followers: - back_to_account: Voltar à conta - title: Seguidores de %{acct} - instances: - by_domain: Domínio - delivery_available: Entrega disponível - known_accounts: - one: "%{count} conta conhecida" - other: "%{count} contas conhecidas" - moderation: - all: Todas - limited: Limitadas - title: Moderação - title: Instâncias conhecidas - total_blocked_by_us: Bloqueadas por nós - total_followed_by_them: Seguidas por eles - total_followed_by_us: Seguidas por nós - total_reported: Relatórios sobre eles - total_storage: Anexos de media - invites: - deactivate_all: Desactivar todos - filter: - all: Todos - available: Disponíveis - expired: Expirados - title: Filtro - title: Convites - relays: - add_new: Adicionar novo repetidor - delete: Apagar - description_html: Um repetidor da federação é um servidor intermediário que troca grandes volumes de publicações públicas entre servidores que o subscrevem e publicam. Ele pode ajudar pequenos e médios servidores a descobrir conteúdo do "fediverse"que, de outro modo, exigiria que os utilizadores locais seguissem manualmente outras pessoas em servidores remotos. - disable: Desactivar - disabled: Desactivado - enable: Activar - enable_hint: Uma vez activado, o teu servidor irá subscrever a todas as publicações deste repetidor e irá começar a enviar as suas publicações públicas para ele. - enabled: Ativado - inbox_url: URL do repetidor - pending: À espera da aprovação do repetidor - save_and_enable: Guardar e ativar - setup: Configurar uma ligação ao repetidor - status: Estado - title: Retransmissores - report_notes: - created_msg: Relatório criado com sucesso! - destroyed_msg: Relatório apagado com sucesso! - reports: - account: - note: nota - report: relatório - action_taken_by: Ação tomada por - are_you_sure: Tens a certeza? - assign_to_self: Atribuí-me a mim - assigned: Atribuído ao moderador - comment: - none: Nenhum - created_at: Relatado - mark_as_resolved: Marcar como resolvido - mark_as_unresolved: Marcar como não resolvido - notes: - create: Adicionar nota - create_and_resolve: Resolver com nota - create_and_unresolve: Reabrir com nota - delete: Apagar - placeholder: Descreve as ações que foram tomadas ou quaisquer outras atualizações relacionadas... - reopen: Reabrir relatório - report: 'Denúncia #%{id}' - reported_account: Conta denunciada - reported_by: Denúnciada por - resolved: Resolvido - resolved_msg: Relatório resolvido com sucesso! - status: Estado - title: Denúncias - unassign: Não atribuir - unresolved: Por resolver - updated_at: Atualizado - settings: - activity_api_enabled: - desc_html: Contagem semanais de publicações locais, utilizadores activos e novos registos - title: Publicar estatísticas agregadas sobre atividade dos utilizadores - bootstrap_timeline_accounts: - desc_html: Separa os nomes de utilizadores por vírgulas. Funciona apenas com contas locais e desbloqueadas. O padrão quando vazio são todos os administradores locais. - title: Seguidores predefinidos para novas contas - contact_information: - email: Inserir um endereço de email para tornar público - username: Insira um nome de utilizador - custom_css: - desc_html: Modificar a aparência com CSS carregado em cada página - title: CSS personalizado - hero: - desc_html: Apresentado na primeira página. Pelo menos 600x100px recomendados. Quando não é definido, é apresentado o thumbnail do servidor - title: Imagem Hero - mascot: - desc_html: Apresentada em múltiplas páginas. Pelo menos 293x205px recomendados. Quando não é definida, é apresentada a mascote predefinida - title: Imagem da mascote - peers_api_enabled: - desc_html: Nomes de domínio que esta instância encontrou no fediverso - title: Publicar lista de instâncias descobertas - preview_sensitive_media: - desc_html: Previsualização de links noutros websites irá apresentar uma miniatura, mesmo que a media seja marcada como sensível - title: Mostrar media sensível em previsualizações OpenGraph - profile_directory: - desc_html: Permite aos utilizadores serem descobertos - title: Ativar directório do perfil - registrations: - closed_message: - desc_html: Mostrar na página inicial quando registos estão encerrados
Podes usar tags HTML - title: Mensagem de registos encerrados - deletion: - desc_html: Permite a qualquer um apagar a conta - title: Permitir eliminar contas - min_invite_role: - disabled: Ninguém - title: Permitir convites de - show_known_fediverse_at_about_page: - desc_html: Quando comutado, irá mostrar a previsualização de publicações de todo o fediverse conhecido. De outro modo só mostrará publicações locais. - title: Mostrar o fediverse conhecido na previsualização da cronologia - show_staff_badge: - desc_html: Mostrar um crachá da equipa na página de utilizador - title: Mostrar crachá da equipa - site_description: - desc_html: Mostrar como parágrafo na página inicial e usado como meta tag.Podes usar tags HTML, em particular <a> e <em>. - title: Descrição do site - site_description_extended: - desc_html: Mostrar na página de mais informações
Podes usar tags HTML - title: Página de mais informações - site_short_description: - desc_html: Mostrada na barra lateral e em etiquetas de metadados. Descreve o que o Mastodon é e o que torna este servidor especial num único parágrafo. Se deixada em branco, remete para a descrição do servidor. - title: Breve descrição do servidor - site_terms: - desc_html: Podes escrever a tua própria política de privacidade, termos de serviço, entre outras coisas. Podes usar tags HTML - title: Termos de serviço customizados - site_title: Título do site - thumbnail: - desc_html: Usada para visualizações via OpenGraph e API. Recomenda-se 1200x630px - title: Miniatura da instância - timeline_preview: - desc_html: Exibir a linha temporal pública na página inicial - title: Visualização da linha temporal - title: Preferências do site - statuses: - back_to_account: Voltar para página da conta - batch: - delete: Eliminar - nsfw_off: NSFW OFF - nsfw_on: NSFW ON - failed_to_execute: Falhou ao executar - media: - title: Média - no_media: Não há média - no_status_selected: Nenhum estado foi alterado porque nenhum foi selecionado - title: Estado das contas - with_media: Com media - title: Administração - warning_presets: - add_new: Adicionar novo - delete: Apagar - edit: Editar - edit_preset: Editar o aviso predefinido - title: Gerir os avisos predefinidos - admin_mailer: - new_report: - body: "%{reporter} relatou %{target}" - body_remote: Alguém de %{domain} relatou %{target} - subject: Novo relatório sobre %{instance} (#%{id}) - application_mailer: - notification_preferences: Alterar preferências de e-mail - settings: 'Alterar preferências de email: %{link}' - view: 'Ver:' - view_profile: Ver perfil - view_status: Ver publicação - applications: - created: Aplicação criada com sucesso - destroyed: Aplicação eliminada com sucesso - invalid_url: O URL é inválido - regenerate_token: Regenerar token de acesso - token_regenerated: Token de acesso regenerado com sucesso - warning: Cuidado com estes dados. Não partilhar com ninguém! - your_token: O teu token de acesso - auth: - change_password: Palavra-passe - delete_account: Eliminar conta - delete_account_html: Se desejas eliminar a conta, podes continua aqui. Uma confirmação será pedida. - didnt_get_confirmation: Não recebeu o email de confirmação? - forgot_password: Esqueceste a palavra-passe? - invalid_reset_password_token: Token de modificação da palavra-passe é inválido ou expirou. Por favor, solicita um novo. - login: Entrar - logout: Sair - migrate_account: Mudar para uma conta diferente - migrate_account_html: Se desejas redirecionar esta conta para uma outra podesconfigurar isso aqui. - or_log_in_with: Ou iniciar sessão com - register: Registar - resend_confirmation: Reenviar instruções de confirmação - reset_password: Criar nova palavra-passe - security: Alterar palavra-passe - set_new_password: Editar palavra-passe - authorize_follow: - already_following: Tu já estás a seguir esta conta - error: Infelizmente, ocorreu um erro ao buscar a conta remota - follow: Seguir - follow_request: 'Enviaste uma solicitação de seguidor para:' - following: 'Sucesso! Agora estás a seguir a:' - post_follow: - close: Ou podes simplesmente fechar esta janela. - return: Voltar ao perfil do utilizador - web: Voltar à página inicial - title: Seguir %{acct} - datetime: - distance_in_words: - about_x_months: "%{count} meses" - about_x_years: "%{count} anos" - almost_x_years: "%{count} anos" - half_a_minute: Justo agora - less_than_x_minutes: "%{count} meses" - less_than_x_seconds: Justo agora - over_x_years: "%{count} anos" - x_days: "%{count} dias" - x_minutes: "%{count} minutos" - 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 - directories: - directory: Dirétorio de perfil - explanation: Descobre utilizadores com base nos seus interesses - explore_mastodon: Explorar %{title} - errors: - '400': The request you submitted was invalid or malformed. - '403': Não tens a permissão necessária para ver esta página. - '404': A página que estás a procurar não existe. - '406': This page is not available in the requested format. - '410': A página que estás a procurar não existe mais. - '422': - content: "A verificação de segurança falhou. \nDesativaste o uso de cookies?" - title: A verificação de segurança falhou - '429': Desacelerado - '500': - content: Desculpe, mas algo correu mal. - title: Esta página não está correta - '503': The page could not be served due to a temporary server failure. - noscript_html: Para usar o aplicativo web do Mastodon, por favor ativa o JavaScript. Alternativamente, experimenta um dos apps nativos para o Mastodon na sua plataforma. - exports: - archive_takeout: - date: Data - download: Descarregar o teu arquivo - hint_html: Podes pedir um arquivo das tuas publicações e ficheiros de media carregados. Os dados do ficheiro exportado estarão no formato ActivityPub, que pode ser lido com qualquer software compatível. Tu podes pedir um arquivo destes a cada 7 dias. - in_progress: A compilar o seu arquivo... - request: Pede o teu arquivo - size: Tamanho - blocks: Bloqueaste - domain_blocks: Bloqueios de domínio - follows: Segues - lists: Listas - mutes: Tens em silêncio - storage: Armazenamento de média - featured_tags: - add_new: Adicionar nova - errors: - limit: Já atingiste o limite máximo de hashtags - filters: - contexts: - home: Cronologia inicial - notifications: Notificações - public: Cronologias públicas - thread: Conversações - edit: - title: Editar filtros - errors: - invalid_context: Inválido ou nenhum contexto fornecido - invalid_irreversible: Filtragem irreversível só funciona no contexto das notificações ou do início - index: - delete: Apagar - title: Filtros - new: - title: Adicionar novo filtro - footer: - developers: Responsáveis pelo desenvolvimento - more: Mais… - resources: Recursos - generic: - changes_saved_msg: Alterações guardadas! - copy: Copiar - save_changes: Guardar alterações - validation_errors: - one: Algo não está correcto. Por favor vê o erro abaixo - other: Algo não está correto. Por favor vê os %{count} erros abaixo - imports: - modes: - merge: Juntar - merge_long: Manter os registos existentes e adicionar novos registos - overwrite: Escrever por cima - overwrite_long: Substituir os registos atuais pelos novos - preface: Podes importar dados que tenhas exportado de outra instância, como a lista de pessoas que segues ou bloqueadas. - success: Os teus dados foram enviados com sucesso e serão processados em breve - types: - blocking: Lista de bloqueio - domain_blocking: Lista de domínios bloqueados - following: Lista de pessoas que estás a seguir - muting: Lista de utilizadores silenciados - upload: Enviar - in_memoriam_html: Em memória. - invites: - delete: Desativar - expired: Expirados - expires_in: - '1800': 30 minutos - '21600': 6 horas - '3600': 1 hora - '43200': 12 horas - '604800': 1 semana - '86400': 1 dia - expires_in_prompt: Nunca - generate: Gerar - invited_by: 'Tu foste convidado por:' - max_uses: - one: 1 uso - other: "%{count} usos" - max_uses_prompt: Sem limite - prompt: Gerar e partilhar ligações com outras pessoas para permitir acesso a essa instância - table: - expires_at: Expira - uses: Usos - title: Convidar pessoas - lists: - errors: - limit: Número máximo de listas alcançado - media_attachments: - validations: - images_and_video: Não é possível anexar um vídeo a uma publicação que já contém imagens - too_many: Não é possível anexar mais de 4 arquivos - migrations: - acct: username@domain da nova conta - currently_redirecting: 'O teu perfil está configurado para redirecionar para:' - proceed: Salvar - updated_msg: As configurações de migração da tua conta foram atualizadas com sucesso! - moderation: - title: Moderação - notification_mailer: - digest: - action: Ver todas as notificações - body: Aqui tens um breve resumo do que perdeste desde o último acesso a %{since} - mention: "%{name} mencionou-te em:" - new_followers_summary: - one: Tens um novo seguidor! Boa! - other: Tens %{count} novos seguidores! Fantástico! - subject: - one: "1 nova notificação desde o último acesso \U0001F418" - other: "%{count} novas notificações desde o último acesso \U0001F418" - title: Enquanto estiveste ausente… - favourite: - body: 'O teu post foi adicionado aos favoritos por %{name}:' - subject: "%{name} adicionou o teu post aos favoritos" - title: Novo favorito - follow: - body: "%{name} é teu seguidor!" - subject: "%{name} começou a seguir-te" - title: Novo seguidor - follow_request: - action: Gerir pedidos de seguidores - body: "%{name} solicitou autorização para te seguir" - subject: 'Seguidor pendente: %{name}' - title: Nova solicitação de seguidor - mention: - action: Responder - body: 'Foste mencionado por %{name}:' - subject: "%{name} mencionou-te" - title: Nova menção - reblog: - body: 'O teu post foi partilhado por %{name}:' - subject: "%{name} partilhou o teu post" - title: Nova partilha - pagination: - newer: Mais nova - next: Seguinte - older: Mais velha - prev: Anterior - polls: - errors: - already_voted: Tu já votaste nesta sondagem - duplicate_options: contém itens duplicados - duration_too_long: está demasiado à frente no futuro - duration_too_short: é demasiado cedo - expired: A sondagem já terminou - over_character_limit: não pode ter mais do que %{max} caracteres cada um - too_few_options: tem de ter mais do que um item - too_many_options: não pode conter mais do que %{max} itens - preferences: - other: Outro - remote_follow: - acct: Entre seu usuário@domínio do qual quer seguir - missing_resource: Não foi possível achar a URL de redirecionamento para sua conta - no_account_html: Não tens uma conta? Tu podes aderir aqui - proceed: Prossiga para seguir - prompt: 'Você vai seguir:' - reason_html: " Porque é que este passo é necessário? %{instance} pode não ser o servidor onde tu estás registado. Por isso, nós precisamos de te redirecionar para o teu servidor inicial em primeiro lugar." - remote_interaction: - favourite: - proceed: Prosseguir para os favoritos - prompt: 'Queres favoritar esta publicação:' - reblog: - proceed: Prosseguir com partilha - prompt: 'Queres partilhar esta publicação:' - reply: - proceed: Prosseguir com resposta - prompt: 'Queres responder a esta publicação:' - scheduled_statuses: - over_daily_limit: Excedeste o limite de %{limit} publicações agendadas para esse dia - over_total_limit: Tu excedeste o limite de %{limit} publicações agendadas - too_soon: A data de agendamento tem de ser futura - sessions: - activity: Última atividade - browser: Navegador - browsers: - generic: Navegador desconhecido - nokia: Navegador Nokia S40 Ovi - otter: Lontra - current_session: Sessão atual - description: "%{browser} em %{platform}" - explanation: Estes são os navegadores que estão conectados com a tua conta do Mastodon. - platforms: - firefox_os: SO Firefox - other: Plataforma desconhecida - revoke: Revogar - revoke_success: Sessão revogada com sucesso - title: Sessões - settings: - authorized_apps: Aplicativos autorizados - back: Voltar ao Mastodon - delete: Eliminação da conta - development: Desenvolvimento - edit_profile: Editar perfil - export: Exportar dados - featured_tags: Hashtags destacadas - import: Importar - migrate: Migração de conta - notifications: Notificações - preferences: Preferências - two_factor_authentication: Autenticação em dois passos - statuses: - attached: - description: 'Anexadas: %{attached}' - image: - one: "%{count} imagem" - other: "%{count} imagens" - video: - one: "%{count} vídeo" - other: "%{count} vídeos" - boosted_from_html: Partilhadas de %{acct_link} - content_warning: 'Aviso de conteúdo: %{warning}' - disallowed_hashtags: - one: 'continha uma hashtag proibida: %{tags}' - other: 'continha as hashtags proibidas: %{tags}' - language_detection: Detectar automaticamente a língua - open_in_web: Abrir no browser - over_character_limit: limite de caracter excedeu %{max} - pin_errors: - limit: Já fixaste a quantidade máxima de publicações - ownership: Posts de outras pessoas não podem ser fixados - private: Post não-público não pode ser fixado - reblog: Não podes fixar uma partilha - poll: - total_votes: - one: "%{count} voto" - other: "%{count} votos" - vote: Votar - show_more: Mostrar mais - sign_in_to_participate: Inicie a sessão para participar na conversa - visibilities: - private: Mostrar apenas para seguidores - private_long: Mostrar apenas para seguidores - public: Público - public_long: Todos podem ver - unlisted: Público, mas não mostre no timeline público - unlisted_long: Todos podem ver, porém não será postado nas timelines públicas - stream_entries: - pinned: Toot fixado - reblogged: partilhado - sensitive_content: Conteúdo sensível - terms: - body_html: | -

Política de privacidade

-

Que informação nós recolhemos?

- -
    -
  • Informação básica da conta: Se te registares neste servidor, pode-te ser pedido que indiques um nome de utilizador, um endereço de e-mail e uma palavra-passe. Também podes introduzir informação adicional de perfil, tal como um nome a mostrar e dados biográficos, que carregues uma fotografia para o teu perfil e para o cabeçalho. O nome de utilizador, o nome a mostrar, a biografia, a imagem de perfil e a imagem de cabeçalho são sempre listados publicamente.
  • -
  • Publicações, seguimento e outra informação pública: A lista de pessoas que tu segues é pública, o mesmo é verdade para os teus seguidores. Quando tu publicas uma mensagem, a data e a hora são guardados, tal como a aplicação a partir da qual a mensagem foi enviada. As mensagens podem conter anexos multimédia, tais como fotografias ou vídeos. Publicações públicas e não listadas são acessíveis publicamente. Quando expões uma publicação no teu perfil, isso é também informação disponível publicamente. As tuas publicações são enviadas aos teus seguidores. Em alguns casos isso significa que elas são enviadas para servidores diferentes onde são guardadas cópias. Quando tu apagas publicações, isso também é enviado para os teus seguidores. A acção de republicar ou favoritar outra publicação é sempre pública.
  • -
  • Publicações directas e exclusivas para seguidores: Todas as publicações são guardadas e processadas no servidor. Publicações exclusivas para seguidores são enviadas para os teus seguidores e para utilizadores que são nelas mencionados. As publicações directas são enviadas apenas para os utilizadores nelas mencionados. Em alguns casos isso significa que elas são enviadas para diferentes servidores onde são guardadas cópias das mesmas. Nós fazemos um grande esforço para limitar o acesso a estas publicações aos utilizadores autorizados, mas outros servidores podem falhar neste objectivo. Por isso, tu deves rever os servidores a que os teus seguidores pertencem. Tu podes activar uma opção para aprovar e rejeitar manualmente novos seguidores nas configurações. Por favor, tem em mente que os gestores do servidor e qualquer servidor que receba a publicação pode lê-lae que os destinatários podem fazer uma captura de tela, copiar ou partilhar a publicação. Não partilhes qualquer informação perigosa no Mastodon.
  • -
  • IPs e outros metadados: Quando inicias sessão, nós guardamos o endereço de IP a partir do qual iniciaste a sessão, tal como o nome do teu navegador. Todas as sessões estão disponíveis para verificação e revogação nas configurações. O último endereço de IP usado é guardado até 12 meses. Nós também podemos guardar registos de servidor, os quais incluem o endereço de IP de cada pedido dirigido ao nosso servidor.
  • -
- -
- -

Para que usamos a tua informação?

- -

Qualquer informação que recolhemos sobre ti pode ser usada dos seguintes modos:

- -
    -
  • Para providenciar a funcionalidade central do Mastodon. Tu só podes interagir com o conteúdo de outras pessoas e publicar o teu próprio conteúdo depois de teres iniciado sessão. Por exemplo, tu podes seguir outras pessoas para veres as suas publicações na tua cronologia inicial personalizada.
  • -
  • Para ajudar na moderação da comunidade para, por exemplo, comparar o teu endereço IP com outros conhecidos, para determinar a fuga ao banimento ou outras violações.
  • -
  • O endereço de e-mail que tu forneces pode ser usado para te enviar informações e/ou notificações sobre outras pessoas que estão a interagir com o teu conteúdo ou a enviar-te mensagens, para responderes a inquéritos e/ou outros pedidos ou questões.
  • -
- -
- -

Como é que nós protegemos a tua informação?

- -

Nós implementamos uma variedade de medidas de segurança para garantir a segurança da tua informação pessoal quando tu introduzes, submetes ou acedes à mesma. Entre outras coisas, a tua sessão de navegação, tal como o tráfego entre as tuas aplicações e a API, estão seguras por SSL e a tua palavra-passe é codificada usando um forte algoritmo de sentido único. Tu podes activar a autenticação em dois passos para aumentares ainda mais a segurança do acesso à tua conta.

- -
- -

Qual é a nossa política de retenção de dados?

- -

Nós envidaremos todos os esforços no sentido de:

- -
    -
  • Guardar registos do servidor contendo o endereço de IP de todos os pedidos feitos a este servidor, considerando que estes registos não serão guardados por mais de 90 dias.
  • -
  • Guardar os endereços de IP associados aos utilizadores registados durante um período que não ultrapassará os 12 meses.
  • -
- -

Tu podes pedir e descarregar um ficheiro com o teu conteúdo, incluindo as tuas publicações, os ficheiros multimédia, a imagem de perfil e a imagem de cabeçalho.

- -

Tu podes apagar a tua conta de modo definitivo e a qualquer momento.

- -
- -

Usamos cookies?

- -

Sim. Cookies são pequenos ficheiros que um site ou o seu fornecedor de serviço transfere para o disco rígido do teu computador através do teu navegador (se tu permitires). Estes cookies permitem ao site reconhecer o teu navegador e, se tu tiveres uma conta registada, associá-lo a ela.

- -

Nós usamos os cookies para compreender e guardar as tuas preferências para as visitas futuras.

- -
- -

Nós divulgamos alguma informação para entidades externas?

- -

Nós não vendemos, trocamos ou transferimos de qualquer modo a tua informação pessoal que seja identificável para qualquer entidade externa. Isto não inclui terceiros de confiança que nos ajudam a manter o nosso site, conduzir o nosso negócio ou prestar-te este serviço, desde que esses terceiros concordem em manter essa informação confidencial. Poderemos também revelar a tua informação quando nós acreditamos que isso é apropriado para cumprir a lei, forçar a aplicação dos nossos termos de serviço ou proteger os direitos, propriedade e segurança, nossos e de outrem.

- -

O teu conteúdo público pode ser descarregado por outros servidores na rede. As tuas publicações públicas e exclusivas para os teus seguidores são enviadas para os servidores onde os teus seguidores residem e as mensagens directas são entregues aos servidores dos seus destinatários, no caso desses seguidores ou destinatários residirem num servidor diferente deste.

- -

Quando tu autorizas uma aplicação a usar a tua conta, dependendo da abrangência das permissões que tu aprovas, ela pode ter acesso à informação pública do teu perfil, à lista de quem segues, aos teus seguidores, às tuas listas, a todas as tuas publicações e aos teus favoritos. As aplicações nunca terão acesso ao teu endereço de e-mail ou à tua palavra-passe.

- -
- -

Utilização do site por crianças

- -

Se este servidor estiver na EU ou na EEA: O nosso site, produtos e serviços são todos dirigidos a pessoas que têm, pelo menos, 16 de idade. Se tu tens menos de 16 anos, devido aos requisitos da GDPR (General Data Protection Regulation) não uses este site.

- -

Se este servidor estiver nos EUA: O nosso site, produtos e serviços são todos dirigidos a pessoas que têm, pelo menos, 13 anos de idade. Se tu tens menos de 13 anos de idade, devido aos requisitos da COPPA (Children's Online Privacy Protection Act) não uses este site.

- -

Os requisitos legais poderão ser diferentes se este servidor estiver noutra jurisdição.

- -
- -

Alterações à nossa Política de Privacidade

- -

Se nós decidirmos alterar a nossa política de privacidade, nós iremos publicar essas alterações nesta página.

- -

Este documento é CC-BY-SA. Ele foi actualizado pela última vez em 7 de Março 2018.

- -

Originalmente adaptado de Discourse privacy policy.

- title: "%{instance} Termos de Serviço e Política de Privacidade" - themes: - contrast: Mastodon (Elevado contraste) - default: Mastodon - mastodon-light: Mastodon (Leve) - two_factor_authentication: - code_hint: Entre o código gerado pelo seu aplicativo para confirmar - description_html: Se ativar a autenticação em dois passos, quando logar será necessário o seu telefone que vai gerar os tokens para validação. - disable: Desativar - enable: Ativar - enabled: A autenticação em dois passos está ativada - enabled_success: Autenticação em dois passos ativada com sucesso - generate_recovery_codes: Gerar códigos para recuperar conta - instructions_html: "Scaneie este código QR no seu Google Authenticator ou aplicativo similar no seu telefone. A partir de agora seu aplicativo irá gerar tokens que deverão ser digitados para você logar." - lost_recovery_codes: Códigos de recuperação permite que você recupere o acesso a sua conta se você perder seu telefone. Se você perder os códigos de recuperação, você pode regera-los aqui. Seus códigos antigos serão invalidados. - manual_instructions: 'Se você não puder scanear o código QR e precisa digita-los manualmente, aqui está o segredo em texto.:' - recovery_codes: Cópia de segurança dos códigos de recuperação - recovery_codes_regenerated: Códigos de recuperação foram gerados com sucesso - recovery_instructions_html: Se tu alguma vez perderes o teu smartphone, to poderás usar um dos códigos de recuperação para voltares a ter acesso à tua conta. Mantém os códigos de recuperação seguros. Por exemplo, tu podes imprimi-los e guardá-los junto a outros documentos importantes. - setup: Configurar - wrong_code: O código inserido é invalido! O horário do servidor e o horário do seu aparelho estão corretos? - user_mailer: - backup_ready: - explanation: Pediste uma cópia completa da tua conta Mastodon. Ela já está pronta para descarregares! - subject: O teu arquivo está pronto para descarregar - title: Arquivo de ficheiros - warning: - explanation: - disable: Enquanto a tua conta está congelada, os seus dados permanecem intactos, mas tu não podes executar quaisquer acções até que ela seja desbloqueada. - silence: Enquanto a tua conta estiver limitada, só pessoas que já estiveres a seguir irão ver as tuas publicações neste servidor e poderás ser excluído de várias listagens públicas. No entanto, outros ainda te poderão seguir de forma manual. - suspend: A tua conta foi suspensa e todas as tuas publicações e os teus ficheiros de media foram irreversivelmente removidos deste servidor e dos servidores onde tinhas seguidores. - review_server_policies: Revê as políticas do servidor - subject: - disable: A tua conta %{acct} foi congelada - none: Aviso para %{acct} - silence: A tua conta %{acct} foi limitada - suspend: A tua conta %{acct} foi suspensa - title: - disable: Conta congelada - none: Aviso - silence: Conta limitada - suspend: Conta suspensa - welcome: - edit_profile_action: Configura o perfil - edit_profile_step: Tu podes personalizar o teu perfil por carregar um avatar, cabeçalho, alterar o teu nickname e mais. Se tu preferires rever os novos seguidores antes deles te poderem seguir, podes bloquear a tua conta. - explanation: Aqui estão algumas dicas para começares - final_action: Começa a publicar - final_step: 'Começa a publicar! Mesmo sem seguidores, as tuas mensagens públicas podem ser vistas por outros, por exemplo, na cronologia local e em hashtags. Tu podes querer apresentar-te na hashtag #introductions.' - full_handle: O teu nome completo - full_handle_hint: Isto é o que tu dirias aos teus amigos para que eles te possam enviar mensagens ou seguir-te a partir de outro servidor. - review_preferences_action: Alterar preferências - review_preferences_step: Certifica-te de configurar as tuas preferências, tais como os e-mails que gostarias de receber ou o nível de privacidade que desejas que as tuas publicações tenham por defeito. Se não sofres de enjoo, podes activar a opção GIF autoplay. - subject: Bem-vindo ao Mastodon - tip_federated_timeline: A cronologia federativa é uma visão global da rede Mastodon. Mas só inclui pessoas que os teus vizinhos subscrevem, por isso não é uma visão completa. - tip_following: Tu segues o(s) administrador(es) do teu servidor por defeito. Para encontrares mais pessoas interessantes, procura nas cronologias local e federativa. - tip_local_timeline: A cronologia local é uma visão global das pessoas em %{instance}. Estes são os teus vizinhos próximos! - tip_mobile_webapp: Se o teu navegador móvel te oferecer a possibilidade de adicionar o Mastodon ao teu homescreen, tu podes receber notificações push. Ele age como uma aplicação nativa de vários modos! - tips: Dicas - title: Bem-vindo a bordo, %{name}! - users: - follow_limit_reached: Não podes seguir mais do que %{limit} pessoas - invalid_email: O endereço de e-mail é inválido - invalid_otp_token: Código de autenticação inválido - otp_lost_help_html: Se tu perdeste acesso a ambos, tu podes entrar em contacto com %{email} - seamless_external_login: Tu estás ligado via um serviço externo. Por isso, as configurações da palavra-passe e do e-mail não estão disponíveis. - signed_in_as: 'Registado como:' - verification: - explanation_html: 'Tu podes comprovar que és o dono dos links nos metadados do teu perfil. Para isso, o website para o qual o link aponta tem de conter um link para o teu perfil do Mastodon. Este link tem de ter um rel="me" atributo. O conteúdo do texto não é relevante. Aqui está um exemplo:' - verification: Verificação diff --git a/config/locales/ro.yml b/config/locales/ro.yml index d04d0015f..7deab6021 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 d1ed8d1de..0c1202118 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 000000000..c7677c850 --- /dev/null +++ b/config/locales/simple_form.br.yml @@ -0,0 +1 @@ +br: diff --git a/config/locales/simple_form.es.yml b/config/locales/simple_form.es.yml index 35cebcad8..898d200d2 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 000000000..777f4e600 --- /dev/null +++ b/config/locales/simple_form.nn.yml @@ -0,0 +1 @@ +nn: diff --git a/config/locales/simple_form.pt-PT.yml b/config/locales/simple_form.pt-PT.yml new file mode 100644 index 000000000..0ac31f8c2 --- /dev/null +++ b/config/locales/simple_form.pt-PT.yml @@ -0,0 +1,127 @@ +--- +pt-PT: + simple_form: + hints: + account_warning_preset: + text: Tu podes usar sintaxe de escrita, como URLs, hashtags e referências + admin_account_action: + send_email_notification: O utilizador receberá uma explicação sobre o que aconteceu com a sua conta + text_html: Opcional. Tu podes usar sintaxe de escrita. Tu podes adicionar predefinições de aviso para poupar tempo + type_html: Escolhe o que fazer com %{acct} + warning_preset_id: Opcional. Tu ainda podes adicionar texto personalizado no fim do predefinido + defaults: + autofollow: As pessoas que aderem através do convite seguir-te-ão automaticamente + avatar: PNG, GIF or JPG. Arquivos até %{size}. Vão ser reduzidos para %{dimensions}px + bot: Esta conta executa essencialmente acções automáticas e pode não poder ser monitorizada + context: Um ou múltiplos contextos nos quais o filtro deve ser aplicado + digest: Enviado após um longo período de inatividade e apenas se foste mencionado na tua ausência + email: Será enviado um e-mail de confirmação + fields: Podes ter até 4 itens expostos, em forma de tabela, no teu perfil + header: PNG, GIF or JPG. Arquivos até %{size}. Vão ser reduzidos para %{dimensions}px + inbox_url: Copia a URL da página inicial do repetidor que queres usar + irreversible: Publicações filtradas irão desaparecer irremediavelmente, mesmo que o filtro seja removido posteriormente + locale: A língua da interface de utilizador, e-mails e notificações push + locked: Requer aprovação manual de seguidores + password: Usa, pelo menos, 8 caracteres + phrase: Será correspondido independentemente da capitalização ou do aviso de conteúdo duma publicação + scopes: Quais as APIs a que será concedido acesso. Se escolheres uma abrangência de nível superior, não precisarás de as seleccionar individualmente. + setting_aggregate_reblogs: Não mostrar novas partilhas que foram partilhadas recentemente (só afecta as novas partilhas) + setting_display_media_default: Esconder media marcada como sensível + setting_display_media_hide_all: Esconder sempre toda a media + setting_display_media_show_all: Mostrar sempre a media marcada como sensível + setting_hide_network: Quem tu segues e quem te segue não será mostrado no teu perfil + setting_noindex: Afecta o teu perfil público e as páginas das tuas publicações + setting_show_application: A aplicação que tu usas para publicar será mostrada na vista detalhada das tuas publicações + username: O teu nome de utilizador será único em %{domain} + whole_word: Quando a palavra-chave ou expressão-chave é somente alfanumérica, ela só será aplicada se corresponder à palavra completa + featured_tag: + name: 'Poderás querer usar um destes:' + imports: + data: Arquivo CSV exportado de outro servidor do Mastodon + sessions: + otp: 'Insere o código de autenticação em dois passos gerado pelo teu telemóvel ou usa um dos teus códigos de recuperação:' + user: + chosen_languages: Quando seleccionado, só publicações nas línguas escolhidas serão mostradas nas cronologias públicas + labels: + account: + fields: + name: Rótulo + value: Conteúdo + account_warning_preset: + text: Texto pré-definido + admin_account_action: + send_email_notification: Notificar o utilizador por e-mail + text: Aviso personalizado + type: Acção + types: + disable: Desactivar + none: Não fazer algo + silence: Silenciar + suspend: Suspender e apagar irreversivelmente os dados da conta + warning_preset_id: Usar um aviso pré-definido + defaults: + autofollow: Convidar para seguir a tua conta + avatar: Imagem de Perfil + bot: Esta é uma conta robô + chosen_languages: Filtrar línguas + confirm_new_password: Confirmar nova palavra-passe + confirm_password: Confirmar palavra-passe + context: Filtrar contextos + current_password: Palavra-passe actual + data: Dados + discoverable: Listar esta conta no directório + display_name: Nome Público + email: Endereço de e-mail + expires_in: Expira em + fields: Meta-dados de perfil + header: Cabeçalho + inbox_url: URL da caixa de entrada do repetidor + irreversible: Expandir em vez de esconder + locale: Idioma + locked: Trancar conta + max_uses: Número máximo de utilizações + new_password: Nova palavra-passe + note: Biografia + otp_attempt: Código de autenticação em dois passos + password: Palavra-passe + phrase: Palavra ou expressão-chave + setting_aggregate_reblogs: Agrupar partilhas em cronologias + setting_auto_play_gif: Reproduzir GIFs automaticamente + setting_boost_modal: Solicitar confirmação antes de partilhar uma publicação + setting_default_language: Língua de publicação + setting_default_privacy: Privacidade da publicação + setting_default_sensitive: Sempre marcar media como sensível + setting_delete_modal: Solicitar confirmação antes de eliminar uma publicação + setting_display_media: Exposição de media + setting_display_media_default: Pré-definição + setting_display_media_hide_all: Esconder todos + setting_display_media_show_all: Mostrar todos + setting_expand_spoilers: Expandir sempre as publicações marcadas com avisos de conteúdo + setting_hide_network: Esconder a tua rede + setting_noindex: Não quero ser indexado por motores de pesquisa + setting_reduce_motion: Reduz movimento em animações + setting_show_application: Revelar sempre qual a aplicação usada para enviar as publicações + setting_system_font_ui: Usar a fonte padrão do teu sistema + setting_theme: Tema do site + setting_unfollow_modal: Solicitar confirmação antes de deixar de seguir alguém + severity: Gravidade + type: Tipo de importação + username: Nome de utilizador + username_or_email: Nome de utilizador ou e-mail + whole_word: Palavra completa + interactions: + must_be_follower: Bloquear notificações de não-seguidores + must_be_following: Bloquear notificações de pessoas que não segues + must_be_following_dm: Bloquear mensagens directas de pessoas que tu não segues + notification_emails: + digest: Enviar e-mails de resumo + favourite: Enviar e-mail quando alguém adiciona uma publicação tua aos favoritos + follow: Enviar e-mail quando alguém te segue + follow_request: Enviar e-mail quando alguém solicita ser teu seguidor + mention: Enviar e-mail quando alguém te menciona + reblog: Enviar e-mail quando alguém partilha uma publicação tua + report: Enviar um e-mail quando um novo relatório é submetido + 'no': Não + required: + text: obrigatório + 'yes': Sim diff --git a/config/locales/simple_form.pt.yml b/config/locales/simple_form.pt.yml deleted file mode 100644 index 9f9d0fdc2..000000000 --- a/config/locales/simple_form.pt.yml +++ /dev/null @@ -1,127 +0,0 @@ ---- -pt: - simple_form: - hints: - account_warning_preset: - text: Tu podes usar sintaxe de escrita, como URLs, hashtags e referências - admin_account_action: - send_email_notification: O utilizador receberá uma explicação sobre o que aconteceu com a sua conta - text_html: Opcional. Tu podes usar sintaxe de escrita. Tu podes adicionar predefinições de aviso para poupar tempo - type_html: Escolhe o que fazer com %{acct} - warning_preset_id: Opcional. Tu ainda podes adicionar texto personalizado no fim do predefinido - defaults: - autofollow: As pessoas que aderem através do convite seguir-te-ão automaticamente - avatar: PNG, GIF or JPG. Arquivos até %{size}. Vão ser reduzidos para %{dimensions}px - bot: Esta conta executa essencialmente acções automáticas e pode não poder ser monitorizada - context: Um ou múltiplos contextos nos quais o filtro deve ser aplicado - digest: Enviado após um longo período de inatividade e apenas se foste mencionado na tua ausência - email: Será enviado um e-mail de confirmação - fields: Podes ter até 4 itens expostos, em forma de tabela, no teu perfil - header: PNG, GIF or JPG. Arquivos até %{size}. Vão ser reduzidos para %{dimensions}px - inbox_url: Copia a URL da página inicial do repetidor que queres usar - irreversible: Publicações filtradas irão desaparecer irremediavelmente, mesmo que o filtro seja removido posteriormente - locale: A língua da interface de utilizador, e-mails e notificações push - locked: Requer aprovação manual de seguidores - password: Usa, pelo menos, 8 caracteres - phrase: Será correspondido independentemente da capitalização ou do aviso de conteúdo duma publicação - scopes: Quais as APIs a que será concedido acesso. Se escolheres uma abrangência de nível superior, não precisarás de as seleccionar individualmente. - setting_aggregate_reblogs: Não mostrar novas partilhas que foram partilhadas recentemente (só afecta as novas partilhas) - setting_display_media_default: Esconder media marcada como sensível - setting_display_media_hide_all: Esconder sempre toda a media - setting_display_media_show_all: Mostrar sempre a media marcada como sensível - setting_hide_network: Quem tu segues e quem te segue não será mostrado no teu perfil - setting_noindex: Afecta o teu perfil público e as páginas das tuas publicações - setting_show_application: A aplicação que tu usas para publicar será mostrada na vista detalhada das tuas publicações - username: O teu nome de utilizador será único em %{domain} - whole_word: Quando a palavra-chave ou expressão-chave é somente alfanumérica, ela só será aplicada se corresponder à palavra completa - featured_tag: - name: 'Poderás querer usar um destes:' - imports: - data: Arquivo CSV exportado de outro servidor do Mastodon - sessions: - otp: 'Insere o código de autenticação em dois passos gerado pelo teu telemóvel ou usa um dos teus códigos de recuperação:' - user: - chosen_languages: Quando seleccionado, só publicações nas línguas escolhidas serão mostradas nas cronologias públicas - labels: - account: - fields: - name: Rótulo - value: Conteúdo - account_warning_preset: - text: Texto pré-definido - admin_account_action: - send_email_notification: Notificar o utilizador por e-mail - text: Aviso personalizado - type: Acção - types: - disable: Desactivar - none: Não fazer algo - silence: Silenciar - suspend: Suspender e apagar irreversivelmente os dados da conta - warning_preset_id: Usar um aviso pré-definido - defaults: - autofollow: Convidar para seguir a tua conta - avatar: Imagem de Perfil - bot: Esta é uma conta robô - chosen_languages: Filtrar línguas - confirm_new_password: Confirmar nova palavra-passe - confirm_password: Confirmar palavra-passe - context: Filtrar contextos - current_password: Palavra-passe actual - data: Dados - discoverable: Listar esta conta no directório - display_name: Nome Público - email: Endereço de e-mail - expires_in: Expira em - fields: Meta-dados de perfil - header: Cabeçalho - inbox_url: URL da caixa de entrada do repetidor - irreversible: Expandir em vez de esconder - locale: Idioma - locked: Trancar conta - max_uses: Número máximo de utilizações - new_password: Nova palavra-passe - note: Biografia - otp_attempt: Código de autenticação em dois passos - password: Palavra-passe - phrase: Palavra ou expressão-chave - setting_aggregate_reblogs: Agrupar partilhas em cronologias - setting_auto_play_gif: Reproduzir GIFs automaticamente - setting_boost_modal: Solicitar confirmação antes de partilhar uma publicação - setting_default_language: Língua de publicação - setting_default_privacy: Privacidade da publicação - setting_default_sensitive: Sempre marcar media como sensível - setting_delete_modal: Solicitar confirmação antes de eliminar uma publicação - setting_display_media: Exposição de media - setting_display_media_default: Pré-definição - setting_display_media_hide_all: Esconder todos - setting_display_media_show_all: Mostrar todos - setting_expand_spoilers: Expandir sempre as publicações marcadas com avisos de conteúdo - setting_hide_network: Esconder a tua rede - setting_noindex: Não quero ser indexado por motores de pesquisa - setting_reduce_motion: Reduz movimento em animações - setting_show_application: Revelar sempre qual a aplicação usada para enviar as publicações - setting_system_font_ui: Usar a fonte padrão do teu sistema - setting_theme: Tema do site - setting_unfollow_modal: Solicitar confirmação antes de deixar de seguir alguém - severity: Gravidade - type: Tipo de importação - username: Nome de utilizador - username_or_email: Nome de utilizador ou e-mail - whole_word: Palavra completa - interactions: - must_be_follower: Bloquear notificações de não-seguidores - must_be_following: Bloquear notificações de pessoas que não segues - must_be_following_dm: Bloquear mensagens directas de pessoas que tu não segues - notification_emails: - digest: Enviar e-mails de resumo - favourite: Enviar e-mail quando alguém adiciona uma publicação tua aos favoritos - follow: Enviar e-mail quando alguém te segue - follow_request: Enviar e-mail quando alguém solicita ser teu seguidor - mention: Enviar e-mail quando alguém te menciona - reblog: Enviar e-mail quando alguém partilha uma publicação tua - report: Enviar um e-mail quando um novo relatório é submetido - 'no': Não - required: - text: obrigatório - 'yes': Sim diff --git a/config/locales/simple_form.ru.yml b/config/locales/simple_form.ru.yml index c4560100a..ab5eb855e 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 980e4613e..e6a30f0c3 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 02507923b..47b835646 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 68754ea24..4e5f37294 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 c4a319964..5c06242cc 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 992311201..772c04d64 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 029704671..a71ea9e18 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 f27c06617..97ef41460 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 564b21db1..5edbbd194 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 d2549bcb4..9c6fd27e8 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 75202fa68..2c59b3f07 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 95f7d7f9a..8bdbf87aa 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: 你的帳戶已經成功刪除 -- cgit From 3ed94dcc1acf73f1d0d1ab43567b88ee953f57c9 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 19 Sep 2019 20:58:19 +0200 Subject: Add account migration UI (#11846) Fix #10736 - Change data export to be available for non-functional accounts - Change non-functional accounts to include redirecting accounts --- .../concerns/export_controller_concern.rb | 7 ++ app/controllers/settings/aliases_controller.rb | 42 +++++++++++ app/controllers/settings/exports_controller.rb | 7 ++ app/controllers/settings/migrations_controller.rb | 48 ++++++++++--- app/helpers/settings_helper.rb | 8 +++ app/models/account_alias.rb | 41 +++++++++++ app/models/account_migration.rb | 74 +++++++++++++++++++ app/models/concerns/account_associations.rb | 2 + app/models/form/migration.rb | 25 ------- app/models/remote_follow.rb | 2 +- app/models/user.rb | 2 +- app/serializers/activitypub/move_serializer.rb | 26 +++++++ app/views/auth/registrations/_status.html.haml | 30 ++++---- app/views/auth/registrations/edit.html.haml | 2 +- app/views/settings/aliases/index.html.haml | 29 ++++++++ app/views/settings/exports/show.html.haml | 4 ++ app/views/settings/migrations/show.html.haml | 84 +++++++++++++++++++--- app/views/settings/profiles/show.html.haml | 5 ++ .../activitypub/move_distribution_worker.rb | 32 +++++++++ config/locales/en.yml | 38 ++++++++-- config/locales/simple_form.en.yml | 10 +++ config/navigation.rb | 8 +-- config/routes.rb | 8 ++- .../20190914202517_create_account_migrations.rb | 12 ++++ .../20190915194355_create_account_aliases.rb | 11 +++ db/schema.rb | 23 ++++++ .../settings/migrations_controller_spec.rb | 14 ++-- spec/fabricators/account_alias_fabricator.rb | 5 ++ spec/fabricators/account_migration_fabricator.rb | 6 ++ spec/models/account_alias_spec.rb | 5 ++ spec/models/account_migration_spec.rb | 5 ++ 31 files changed, 542 insertions(+), 73 deletions(-) create mode 100644 app/controllers/settings/aliases_controller.rb create mode 100644 app/models/account_alias.rb create mode 100644 app/models/account_migration.rb delete mode 100644 app/models/form/migration.rb create mode 100644 app/serializers/activitypub/move_serializer.rb create mode 100644 app/views/settings/aliases/index.html.haml create mode 100644 app/workers/activitypub/move_distribution_worker.rb create mode 100644 db/migrate/20190914202517_create_account_migrations.rb create mode 100644 db/migrate/20190915194355_create_account_aliases.rb create mode 100644 spec/fabricators/account_alias_fabricator.rb create mode 100644 spec/fabricators/account_migration_fabricator.rb create mode 100644 spec/models/account_alias_spec.rb create mode 100644 spec/models/account_migration_spec.rb (limited to 'app/helpers') diff --git a/app/controllers/concerns/export_controller_concern.rb b/app/controllers/concerns/export_controller_concern.rb index e20b71a30..bfe990c82 100644 --- a/app/controllers/concerns/export_controller_concern.rb +++ b/app/controllers/concerns/export_controller_concern.rb @@ -5,7 +5,10 @@ module ExportControllerConcern included do before_action :authenticate_user! + before_action :require_not_suspended! before_action :load_export + + skip_before_action :require_functional! end private @@ -27,4 +30,8 @@ module ExportControllerConcern def export_filename "#{controller_name}.csv" end + + def require_not_suspended! + forbidden if current_account.suspended? + end end diff --git a/app/controllers/settings/aliases_controller.rb b/app/controllers/settings/aliases_controller.rb new file mode 100644 index 000000000..2b675f065 --- /dev/null +++ b/app/controllers/settings/aliases_controller.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +class Settings::AliasesController < Settings::BaseController + layout 'admin' + + before_action :authenticate_user! + before_action :set_aliases, except: :destroy + before_action :set_alias, only: :destroy + + def index + @alias = current_account.aliases.build + end + + def create + @alias = current_account.aliases.build(resource_params) + + if @alias.save + redirect_to settings_aliases_path, notice: I18n.t('aliases.created_msg') + else + render :show + end + end + + def destroy + @alias.destroy! + redirect_to settings_aliases_path, notice: I18n.t('aliases.deleted_msg') + end + + private + + def resource_params + params.require(:account_alias).permit(:acct) + end + + def set_alias + @alias = current_account.aliases.find(params[:id]) + end + + def set_aliases + @aliases = current_account.aliases.order(id: :desc).reject(&:new_record?) + end +end diff --git a/app/controllers/settings/exports_controller.rb b/app/controllers/settings/exports_controller.rb index 3012fbf77..0e93d07a9 100644 --- a/app/controllers/settings/exports_controller.rb +++ b/app/controllers/settings/exports_controller.rb @@ -6,6 +6,9 @@ class Settings::ExportsController < Settings::BaseController layout 'admin' before_action :authenticate_user! + before_action :require_not_suspended! + + skip_before_action :require_functional! def show @export = Export.new(current_account) @@ -34,4 +37,8 @@ class Settings::ExportsController < Settings::BaseController def lock_options { redis: Redis.current, key: "backup:#{current_user.id}" } end + + def require_not_suspended! + forbidden if current_account.suspended? + end end diff --git a/app/controllers/settings/migrations_controller.rb b/app/controllers/settings/migrations_controller.rb index 59eb48779..90092c692 100644 --- a/app/controllers/settings/migrations_controller.rb +++ b/app/controllers/settings/migrations_controller.rb @@ -4,31 +4,59 @@ class Settings::MigrationsController < Settings::BaseController layout 'admin' before_action :authenticate_user! + before_action :require_not_suspended! + before_action :set_migrations + before_action :set_cooldown + + skip_before_action :require_functional! def show - @migration = Form::Migration.new(account: current_account.moved_to_account) + @migration = current_account.migrations.build end - def update - @migration = Form::Migration.new(resource_params) + def create + @migration = current_account.migrations.build(resource_params) - if @migration.valid? && migration_account_changed? - current_account.update!(moved_to_account: @migration.account) + if @migration.save_with_challenge(current_user) + current_account.update!(moved_to_account: @migration.target_account) ActivityPub::UpdateDistributionWorker.perform_async(current_account.id) - redirect_to settings_migration_path, notice: I18n.t('migrations.updated_msg') + ActivityPub::MoveDistributionWorker.perform_async(@migration.id) + redirect_to settings_migration_path, notice: I18n.t('migrations.moved_msg', acct: current_account.moved_to_account.acct) else render :show end end + def cancel + if current_account.moved_to_account_id.present? + current_account.update!(moved_to_account: nil) + ActivityPub::UpdateDistributionWorker.perform_async(current_account.id) + end + + redirect_to settings_migration_path, notice: I18n.t('migrations.cancelled_msg') + end + + helper_method :on_cooldown? + private def resource_params - params.require(:migration).permit(:acct) + params.require(:account_migration).permit(:acct, :current_password, :current_username) + end + + def set_migrations + @migrations = current_account.migrations.includes(:target_account).order(id: :desc).reject(&:new_record?) + end + + def set_cooldown + @cooldown = current_account.migrations.within_cooldown.first + end + + def on_cooldown? + @cooldown.present? end - def migration_account_changed? - current_account.moved_to_account_id != @migration.account&.id && - current_account.id != @migration.account&.id + def require_not_suspended! + forbidden if current_account.suspended? end end diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 2b3fd1263..ecc73baf5 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -87,4 +87,12 @@ module SettingsHelper 'desktop' end end + + def compact_account_link_to(account) + return if account.nil? + + link_to ActivityPub::TagManager.instance.url_for(account), class: 'name-tag', title: account.acct do + safe_join([image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'), content_tag(:span, account.acct, class: 'username')], ' ') + end + end end diff --git a/app/models/account_alias.rb b/app/models/account_alias.rb new file mode 100644 index 000000000..e9a0dd79e --- /dev/null +++ b/app/models/account_alias.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: account_aliases +# +# id :bigint(8) not null, primary key +# account_id :bigint(8) +# acct :string default(""), not null +# uri :string default(""), not null +# created_at :datetime not null +# updated_at :datetime not null +# + +class AccountAlias < ApplicationRecord + belongs_to :account + + validates :acct, presence: true, domain: { acct: true } + validates :uri, presence: true + + before_validation :set_uri + after_create :add_to_account + after_destroy :remove_from_account + + private + + def set_uri + target_account = ResolveAccountService.new.call(acct) + self.uri = ActivityPub::TagManager.instance.uri_for(target_account) unless target_account.nil? + rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error + # Validation will take care of it + end + + def add_to_account + account.update(also_known_as: account.also_known_as + [uri]) + end + + def remove_from_account + account.update(also_known_as: account.also_known_as.reject { |x| x == uri }) + end +end diff --git a/app/models/account_migration.rb b/app/models/account_migration.rb new file mode 100644 index 000000000..15830bffb --- /dev/null +++ b/app/models/account_migration.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: account_migrations +# +# id :bigint(8) not null, primary key +# account_id :bigint(8) +# acct :string default(""), not null +# followers_count :bigint(8) default(0), not null +# target_account_id :bigint(8) +# created_at :datetime not null +# updated_at :datetime not null +# + +class AccountMigration < ApplicationRecord + COOLDOWN_PERIOD = 30.days.freeze + + belongs_to :account + belongs_to :target_account, class_name: 'Account' + + before_validation :set_target_account + before_validation :set_followers_count + + validates :acct, presence: true, domain: { acct: true } + validate :validate_migration_cooldown + validate :validate_target_account + + scope :within_cooldown, ->(now = Time.now.utc) { where(arel_table[:created_at].gteq(now - COOLDOWN_PERIOD)) } + + attr_accessor :current_password, :current_username + + def save_with_challenge(current_user) + if current_user.encrypted_password.present? + errors.add(:current_password, :invalid) unless current_user.valid_password?(current_password) + else + errors.add(:current_username, :invalid) unless account.username == current_username + end + + return false unless errors.empty? + + save + end + + def cooldown_at + created_at + COOLDOWN_PERIOD + end + + private + + def set_target_account + self.target_account = ResolveAccountService.new.call(acct) + rescue Goldfinger::Error, HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::Error + # Validation will take care of it + end + + def set_followers_count + self.followers_count = account.followers_count + end + + def validate_target_account + if target_account.nil? + errors.add(:acct, I18n.t('migrations.errors.not_found')) + else + errors.add(:acct, I18n.t('migrations.errors.missing_also_known_as')) unless target_account.also_known_as.include?(ActivityPub::TagManager.instance.uri_for(account)) + errors.add(:acct, I18n.t('migrations.errors.already_moved')) if account.moved_to_account_id.present? && account.moved_to_account_id == target_account.id + errors.add(:acct, I18n.t('migrations.errors.move_to_self')) if account.id == target_account.id + end + end + + def validate_migration_cooldown + errors.add(:base, I18n.t('migrations.errors.on_cooldown')) if account.migrations.within_cooldown.exists? + end +end diff --git a/app/models/concerns/account_associations.rb b/app/models/concerns/account_associations.rb index 1db7771c7..c9cc5c610 100644 --- a/app/models/concerns/account_associations.rb +++ b/app/models/concerns/account_associations.rb @@ -52,6 +52,8 @@ module AccountAssociations # Account migrations belongs_to :moved_to_account, class_name: 'Account', optional: true + has_many :migrations, class_name: 'AccountMigration', dependent: :destroy, inverse_of: :account + has_many :aliases, class_name: 'AccountAlias', dependent: :destroy, inverse_of: :account # Hashtags has_and_belongs_to_many :tags diff --git a/app/models/form/migration.rb b/app/models/form/migration.rb deleted file mode 100644 index c2a8655e1..000000000 --- a/app/models/form/migration.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -class Form::Migration - include ActiveModel::Validations - - attr_accessor :acct, :account - - def initialize(attrs = {}) - @account = attrs[:account] - @acct = attrs[:account].acct unless @account.nil? - @acct = attrs[:acct].gsub(/\A@/, '').strip unless attrs[:acct].nil? - end - - def valid? - return false unless super - set_account - errors.empty? - end - - private - - def set_account - self.account = (ResolveAccountService.new.call(acct) if account.nil? && acct.present?) - end -end diff --git a/app/models/remote_follow.rb b/app/models/remote_follow.rb index 52dd3f67b..5ea535287 100644 --- a/app/models/remote_follow.rb +++ b/app/models/remote_follow.rb @@ -49,7 +49,7 @@ class RemoteFollow end def fetch_template! - return missing_resource if acct.blank? + return missing_resource_error if acct.blank? _, domain = acct.split('@') diff --git a/app/models/user.rb b/app/models/user.rb index b48455802..9a19a53b3 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -168,7 +168,7 @@ class User < ApplicationRecord end def functional? - confirmed? && approved? && !disabled? && !account.suspended? + confirmed? && approved? && !disabled? && !account.suspended? && account.moved_to_account_id.nil? end def unconfirmed_or_pending? diff --git a/app/serializers/activitypub/move_serializer.rb b/app/serializers/activitypub/move_serializer.rb new file mode 100644 index 000000000..5675875fa --- /dev/null +++ b/app/serializers/activitypub/move_serializer.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class ActivityPub::MoveSerializer < ActivityPub::Serializer + attributes :id, :type, :target, :actor + attribute :virtual_object, key: :object + + def id + [ActivityPub::TagManager.instance.uri_for(object.account), '#moves/', object.id].join + end + + def type + 'Move' + end + + def target + ActivityPub::TagManager.instance.uri_for(object.target_account) + end + + def virtual_object + ActivityPub::TagManager.instance.uri_for(object.account) + end + + def actor + ActivityPub::TagManager.instance.uri_for(object.account) + end +end diff --git a/app/views/auth/registrations/_status.html.haml b/app/views/auth/registrations/_status.html.haml index b38a83d67..47112dae0 100644 --- a/app/views/auth/registrations/_status.html.haml +++ b/app/views/auth/registrations/_status.html.haml @@ -1,16 +1,22 @@ %h3= t('auth.status.account_status') -- if @user.account.suspended? - %span.negative-hint= t('user_mailer.warning.explanation.suspend') -- elsif @user.disabled? - %span.negative-hint= t('user_mailer.warning.explanation.disable') -- elsif @user.account.silenced? - %span.warning-hint= t('user_mailer.warning.explanation.silence') -- elsif !@user.confirmed? - %span.warning-hint= t('auth.status.confirming') -- elsif !@user.approved? - %span.warning-hint= t('auth.status.pending') -- else - %span.positive-hint= t('auth.status.functional') +.simple_form + %p.hint + - if @user.account.suspended? + %span.negative-hint= t('user_mailer.warning.explanation.suspend') + - elsif @user.disabled? + %span.negative-hint= t('user_mailer.warning.explanation.disable') + - elsif @user.account.silenced? + %span.warning-hint= t('user_mailer.warning.explanation.silence') + - elsif !@user.confirmed? + %span.warning-hint= t('auth.status.confirming') + = link_to t('auth.didnt_get_confirmation'), new_user_confirmation_path + - elsif !@user.approved? + %span.warning-hint= t('auth.status.pending') + - elsif @user.account.moved_to_account_id.present? + %span.positive-hint= t('auth.status.redirecting_to', acct: @user.account.moved_to_account.acct) + = link_to t('migrations.cancel'), settings_migration_path + - else + %span.positive-hint= t('auth.status.functional') %hr.spacer/ diff --git a/app/views/auth/registrations/edit.html.haml b/app/views/auth/registrations/edit.html.haml index 710ee5c68..885171c58 100644 --- a/app/views/auth/registrations/edit.html.haml +++ b/app/views/auth/registrations/edit.html.haml @@ -13,7 +13,7 @@ .fields-row__column.fields-group.fields-row__column-6 = f.input :email, wrapper: :with_label, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }, required: true, disabled: current_account.suspended? .fields-row__column.fields-group.fields-row__column-6 - = f.input :current_password, wrapper: :with_label, input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password'), :autocomplete => 'off' }, required: true, disabled: current_account.suspended? + = f.input :current_password, wrapper: :with_label, input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password'), :autocomplete => 'off' }, required: true, disabled: current_account.suspended?, hint: false .fields-row .fields-row__column.fields-group.fields-row__column-6 diff --git a/app/views/settings/aliases/index.html.haml b/app/views/settings/aliases/index.html.haml new file mode 100644 index 000000000..5b6986368 --- /dev/null +++ b/app/views/settings/aliases/index.html.haml @@ -0,0 +1,29 @@ +- content_for :page_title do + = t('settings.aliases') + += simple_form_for @alias, url: settings_aliases_path do |f| + = render 'shared/error_messages', object: @alias + + %p.hint= t('aliases.hint_html') + + %hr.spacer/ + + .fields-group + = f.input :acct, wrapper: :with_block_label, input_html: { autocapitalize: 'none', autocorrect: 'off' } + + .actions + = f.button :button, t('aliases.add_new'), type: :submit, class: 'button' + +%hr.spacer/ + +.table-wrapper + %table.table.inline-table + %thead + %tr + %th= t('simple_form.labels.account_alias.acct') + %th + %tbody + - @aliases.each do |account_alias| + %tr + %td= account_alias.acct + %td= table_link_to 'trash', t('aliases.remove'), settings_alias_path(account_alias), data: { method: :delete } diff --git a/app/views/settings/exports/show.html.haml b/app/views/settings/exports/show.html.haml index b13cea976..76ff76bd9 100644 --- a/app/views/settings/exports/show.html.haml +++ b/app/views/settings/exports/show.html.haml @@ -37,12 +37,16 @@ %td= number_with_delimiter @export.total_domain_blocks %td= table_link_to 'download', t('exports.csv'), settings_exports_domain_blocks_path(format: :csv) +%hr.spacer/ + %p.muted-hint= t('exports.archive_takeout.hint_html') - if policy(:backup).create? %p= link_to t('exports.archive_takeout.request'), settings_export_path, class: 'button', method: :post - unless @backups.empty? + %hr.spacer/ + .table-wrapper %table.table %thead diff --git a/app/views/settings/migrations/show.html.haml b/app/views/settings/migrations/show.html.haml index c69061d50..1e5c47726 100644 --- a/app/views/settings/migrations/show.html.haml +++ b/app/views/settings/migrations/show.html.haml @@ -1,17 +1,85 @@ - content_for :page_title do = t('settings.migrate') -= simple_form_for @migration, as: :migration, url: settings_migration_path, html: { method: :put } do |f| - - if @migration.account - %p.hint= t('migrations.currently_redirecting') +.simple_form + - if current_account.moved_to_account.present? + .fields-row + .fields-row__column.fields-group.fields-row__column-6 + = render 'application/card', account: current_account.moved_to_account + .fields-row__column.fields-group.fields-row__column-6 + %p.hint + %span.positive-hint= t('migrations.redirecting_to', acct: current_account.moved_to_account.acct) - .fields-group - = render partial: 'application/card', locals: { account: @migration.account } + %p.hint= t('migrations.cancel_explanation') + + %p.hint= link_to t('migrations.cancel'), cancel_settings_migration_path, data: { method: :post } + - else + %p.hint + %span.positive-hint= t('migrations.not_redirecting') + +%hr.spacer/ + +%h3= t 'migrations.proceed_with_move' + += simple_form_for @migration, url: settings_migration_path do |f| + - if on_cooldown? + %span.warning-hint= t('migrations.on_cooldown', count: ((@cooldown.cooldown_at - Time.now.utc) / 1.day.seconds).ceil) + - else + %p.hint= t('migrations.warning.before') + + %ul.hint + %li.warning-hint= t('migrations.warning.followers') + %li.warning-hint= t('migrations.warning.other_data') + %li.warning-hint= t('migrations.warning.backreference_required') + %li.warning-hint= t('migrations.warning.cooldown') + %li.warning-hint= t('migrations.warning.disabled_account') + + %hr.spacer/ = render 'shared/error_messages', object: @migration - .fields-group - = f.input :acct, placeholder: t('migrations.acct') + .fields-row + .fields-row__column.fields-group.fields-row__column-6 + = f.input :acct, wrapper: :with_block_label, input_html: { autocapitalize: 'none', autocorrect: 'off' }, disabled: on_cooldown? + + .fields-row__column.fields-group.fields-row__column-6 + - if current_user.encrypted_password.present? + = f.input :current_password, wrapper: :with_block_label, input_html: { :autocomplete => 'off' }, required: true, disabled: on_cooldown? + - else + = f.input :current_username, wrapper: :with_block_label, input_html: { :autocomplete => 'off' }, required: true, disabled: on_cooldown? .actions - = f.button :button, t('migrations.proceed'), type: :submit, class: 'negative' + = f.button :button, t('migrations.proceed_with_move'), type: :submit, class: 'button button--destructive', disabled: on_cooldown? + +- unless @migrations.empty? + %hr.spacer/ + + %h3= t 'migrations.past_migrations' + + %hr.spacer/ + + .table-wrapper + %table.table.inline-table + %thead + %tr + %th= t('migrations.acct') + %th= t('migrations.followers_count') + %th + %tbody + - @migrations.each do |migration| + %tr + %td + - if migration.target_account.present? + = compact_account_link_to migration.target_account + - else + = migration.acct + + %td= number_with_delimiter migration.followers_count + + %td + %time.time-ago{ datetime: migration.created_at.iso8601, title: l(migration.created_at) }= l(migration.created_at) + +%hr.spacer/ + +%h3= t 'migrations.incoming_migrations' +%p.muted-hint= t('migrations.incoming_migrations_html', path: settings_aliases_path) diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml index f042011d6..6929f54f3 100644 --- a/app/views/settings/profiles/show.html.haml +++ b/app/views/settings/profiles/show.html.haml @@ -60,6 +60,11 @@ %h6= t('auth.migrate_account') %p.muted-hint= t('auth.migrate_account_html', path: settings_migration_path) +%hr.spacer/ + +%h6= t 'migrations.incoming_migrations' +%p.muted-hint= t('migrations.incoming_migrations_html', path: settings_aliases_path) + - if open_deletion? %hr.spacer/ diff --git a/app/workers/activitypub/move_distribution_worker.rb b/app/workers/activitypub/move_distribution_worker.rb new file mode 100644 index 000000000..396d5258f --- /dev/null +++ b/app/workers/activitypub/move_distribution_worker.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class ActivityPub::MoveDistributionWorker + include Sidekiq::Worker + include Payloadable + + sidekiq_options queue: 'push' + + def perform(migration_id) + @migration = AccountMigration.find(migration_id) + + ActivityPub::DeliveryWorker.push_bulk(inboxes) do |inbox_url| + [signed_payload, @account.id, inbox_url] + end + + ActivityPub::DeliveryWorker.push_bulk(Relay.enabled.pluck(:inbox_url)) do |inbox_url| + [signed_payload, @account.id, inbox_url] + end + rescue ActiveRecord::RecordNotFound + true + end + + private + + def inboxes + @inboxes ||= @migration.account.followers.inboxes + end + + def signed_payload + @signed_payload ||= Oj.dump(serialize_payload(@migration, ActivityPub::MoveSerializer, signer: @account)) + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index dabb679e7..c29c7f871 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -554,6 +554,12 @@ en: new_trending_tag: body: 'The hashtag #%{name} is trending today, but has not been previously reviewed. It will not be displayed publicly unless you allow it to, or just save the form as it is to never hear about it again.' subject: New hashtag up for review on %{instance} (#%{name}) + aliases: + add_new: Create alias + created_msg: Successfully created a new alias. You can now initiate the move from the old account. + deleted_msg: Successfully remove the alias. Moving from that account to this one will no longer be possible. + hint_html: If you want to move from another account to this one, here you can create an alias, which is required before you can proceed with moving followers from the old account to this one. This action by itself is harmless and reversible. The account migration is initiated from the old account. + remove: Unlink alias appearance: advanced_web_interface: Advanced web interface advanced_web_interface_hint: 'If you want to make use of your entire screen width, the advanced web interface allows you to configure many different columns to see as much information at the same time as you want: Home, notifications, federated timeline, any number of lists and hashtags.' @@ -613,6 +619,7 @@ en: confirming: Waiting for e-mail confirmation to be completed. functional: Your account is fully operational. pending: Your application is pending review by our staff. This may take some time. You will receive an e-mail if your application is approved. + redirecting_to: Your account is inactive because it is currently redirecting to %{acct}. trouble_logging_in: Trouble logging in? authorize_follow: already_following: You are already following this account @@ -801,10 +808,32 @@ en: images_and_video: Cannot attach a video to a status that already contains images too_many: Cannot attach more than 4 files migrations: - acct: username@domain of the new account - currently_redirecting: 'Your profile is set to redirect to:' - proceed: Save - updated_msg: Your account migration setting successfully updated! + acct: Moved to + cancel: Cancel redirect + cancel_explanation: Cancelling the redirect will re-activate your current account, but will not bring back followers that have been moved to that account. + cancelled_msg: Successfully cancelled the redirect. + errors: + already_moved: is the same account you have already moved to + missing_also_known_as: is not back-referencing this account + move_to_self: cannot be current account + not_found: could not be found + on_cooldown: You are on cooldown + followers_count: Followers at time of move + incoming_migrations: Moving from a different account + incoming_migrations_html: To move from another account to this one, first you need to create an account alias. + moved_msg: Your account is now redirecting to %{acct} and your followers are being moved over. + not_redirecting: Your account is not redirecting to any other account currently. + on_cooldown: You have recently migrated your account. This function will become available again in %{count} days. + past_migrations: Past migrations + proceed_with_move: Move followers + redirecting_to: Your account is redirecting to %{acct}. + warning: + backreference_required: The new account must first be configured to back-reference this one + before: 'Before proceeding, please read these notes carefully:' + cooldown: After moving there is a cooldown period during which you will not be able to move again + disabled_account: Your current account will not be fully usable afterwards. However, you will have access to data export as well as re-activation. + followers: This action will move all followers from the current account to the new account + other_data: No other data will be moved automatically moderation: title: Moderation notification_mailer: @@ -950,6 +979,7 @@ en: settings: account: Account account_settings: Account settings + aliases: Account aliases appearance: Appearance authorized_apps: Authorized apps back: Back to Mastodon diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index c9ffcfc13..3d909e999 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -2,6 +2,10 @@ en: simple_form: hints: + account_alias: + acct: Specify the username@domain of the account you want to move from + account_migration: + acct: Specify the username@domain of the account you want to move to account_warning_preset: text: You can use toot syntax, such as URLs, hashtags and mentions admin_account_action: @@ -15,6 +19,8 @@ en: avatar: PNG, GIF or JPG. At most %{size}. Will be downscaled to %{dimensions}px bot: This account mainly performs automated actions and might not be monitored context: One or multiple contexts where the filter should apply + current_password: For security purposes please enter the password of the current account + current_username: To confirm, please enter the username of the current account digest: Only sent after a long period of inactivity and only if you have received any personal messages in your absence discoverable: The profile directory is another way by which your account can reach a wider audience email: You will be sent a confirmation e-mail @@ -60,6 +66,10 @@ en: fields: name: Label value: Content + account_alias: + acct: Handle of the old account + account_migration: + acct: Handle of the new account account_warning_preset: text: Preset text admin_account_action: diff --git a/config/navigation.rb b/config/navigation.rb index 38668bbf7..32c299143 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -5,7 +5,7 @@ SimpleNavigation::Configuration.run do |navigation| n.item :web, safe_join([fa_icon('chevron-left fw'), t('settings.back')]), root_url n.item :profile, safe_join([fa_icon('user fw'), t('settings.profile')]), settings_profile_url, if: -> { current_user.functional? } do |s| - s.item :profile, safe_join([fa_icon('pencil fw'), t('settings.appearance')]), settings_profile_url, highlights_on: %r{/settings/profile|/settings/migration} + s.item :profile, safe_join([fa_icon('pencil fw'), t('settings.appearance')]), settings_profile_url s.item :featured_tags, safe_join([fa_icon('hashtag fw'), t('settings.featured_tags')]), settings_featured_tags_url s.item :identity_proofs, safe_join([fa_icon('key fw'), t('settings.identity_proofs')]), settings_identity_proofs_path, highlights_on: %r{/settings/identity_proofs*}, if: proc { current_account.identity_proofs.exists? } end @@ -20,13 +20,13 @@ SimpleNavigation::Configuration.run do |navigation| n.item :filters, safe_join([fa_icon('filter fw'), t('filters.index.title')]), filters_path, highlights_on: %r{/filters}, if: -> { current_user.functional? } n.item :security, safe_join([fa_icon('lock fw'), t('settings.account')]), edit_user_registration_url do |s| - s.item :password, safe_join([fa_icon('lock fw'), t('settings.account_settings')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete} + s.item :password, safe_join([fa_icon('lock fw'), t('settings.account_settings')]), edit_user_registration_url, highlights_on: %r{/auth/edit|/settings/delete|/settings/migration|/settings/aliases} s.item :two_factor_authentication, safe_join([fa_icon('mobile fw'), t('settings.two_factor_authentication')]), settings_two_factor_authentication_url, highlights_on: %r{/settings/two_factor_authentication} s.item :authorized_apps, safe_join([fa_icon('list fw'), t('settings.authorized_apps')]), oauth_authorized_applications_url end - n.item :data, safe_join([fa_icon('cloud-download fw'), t('settings.import_and_export')]), settings_export_url, if: -> { current_user.functional? } do |s| - s.item :import, safe_join([fa_icon('cloud-upload fw'), t('settings.import')]), settings_import_url + n.item :data, safe_join([fa_icon('cloud-download fw'), t('settings.import_and_export')]), settings_export_url do |s| + s.item :import, safe_join([fa_icon('cloud-upload fw'), t('settings.import')]), settings_import_url, if: -> { current_user.functional? } s.item :export, safe_join([fa_icon('cloud-download fw'), t('settings.export')]), settings_export_url end diff --git a/config/routes.rb b/config/routes.rb index dcfa079a0..37e0cbdee 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -134,8 +134,14 @@ Rails.application.routes.draw do end resource :delete, only: [:show, :destroy] - resource :migration, only: [:show, :update] + resource :migration, only: [:show, :create] do + collection do + post :cancel + end + end + + resources :aliases, only: [:index, :create, :destroy] resources :sessions, only: [:destroy] resources :featured_tags, only: [:index, :create, :destroy] end diff --git a/db/migrate/20190914202517_create_account_migrations.rb b/db/migrate/20190914202517_create_account_migrations.rb new file mode 100644 index 000000000..cb9d71c09 --- /dev/null +++ b/db/migrate/20190914202517_create_account_migrations.rb @@ -0,0 +1,12 @@ +class CreateAccountMigrations < ActiveRecord::Migration[5.2] + def change + create_table :account_migrations do |t| + t.belongs_to :account, foreign_key: { on_delete: :cascade } + t.string :acct, null: false, default: '' + t.bigint :followers_count, null: false, default: 0 + t.belongs_to :target_account, foreign_key: { to_table: :accounts, on_delete: :nullify } + + t.timestamps + end + end +end diff --git a/db/migrate/20190915194355_create_account_aliases.rb b/db/migrate/20190915194355_create_account_aliases.rb new file mode 100644 index 000000000..32ce031d9 --- /dev/null +++ b/db/migrate/20190915194355_create_account_aliases.rb @@ -0,0 +1,11 @@ +class CreateAccountAliases < ActiveRecord::Migration[5.2] + def change + create_table :account_aliases do |t| + t.belongs_to :account, foreign_key: { on_delete: :cascade } + t.string :acct, null: false, default: '' + t.string :uri, null: false, default: '' + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 749f79dee..fabeb16f3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -15,6 +15,15 @@ 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" + create_table "account_aliases", force: :cascade do |t| + t.bigint "account_id" + t.string "acct", default: "", null: false + t.string "uri", default: "", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id"], name: "index_account_aliases_on_account_id" + end + create_table "account_conversations", force: :cascade do |t| t.bigint "account_id" t.bigint "conversation_id" @@ -49,6 +58,17 @@ ActiveRecord::Schema.define(version: 2019_09_17_213523) do t.index ["account_id"], name: "index_account_identity_proofs_on_account_id" end + create_table "account_migrations", force: :cascade do |t| + t.bigint "account_id" + t.string "acct", default: "", null: false + t.bigint "followers_count", default: 0, null: false + t.bigint "target_account_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id"], name: "index_account_migrations_on_account_id" + t.index ["target_account_id"], name: "index_account_migrations_on_target_account_id" + end + create_table "account_moderation_notes", force: :cascade do |t| t.text "content", null: false t.bigint "account_id", null: false @@ -768,10 +788,13 @@ ActiveRecord::Schema.define(version: 2019_09_17_213523) do t.index ["user_id"], name: "index_web_settings_on_user_id", unique: true end + add_foreign_key "account_aliases", "accounts", on_delete: :cascade add_foreign_key "account_conversations", "accounts", on_delete: :cascade add_foreign_key "account_conversations", "conversations", on_delete: :cascade add_foreign_key "account_domain_blocks", "accounts", name: "fk_206c6029bd", on_delete: :cascade add_foreign_key "account_identity_proofs", "accounts", on_delete: :cascade + add_foreign_key "account_migrations", "accounts", column: "target_account_id", on_delete: :nullify + add_foreign_key "account_migrations", "accounts", on_delete: :cascade add_foreign_key "account_moderation_notes", "accounts" add_foreign_key "account_moderation_notes", "accounts", column: "target_account_id" add_foreign_key "account_pins", "accounts", column: "target_account_id", on_delete: :cascade diff --git a/spec/controllers/settings/migrations_controller_spec.rb b/spec/controllers/settings/migrations_controller_spec.rb index 4d814a45e..36e4ba86e 100644 --- a/spec/controllers/settings/migrations_controller_spec.rb +++ b/spec/controllers/settings/migrations_controller_spec.rb @@ -21,6 +21,7 @@ describe Settings::MigrationsController do let(:user) { Fabricate(:user, account: account) } let(:account) { Fabricate(:account, moved_to_account: moved_to_account) } + before { sign_in user, scope: :user } context 'when user does not have moved to account' do @@ -32,7 +33,7 @@ describe Settings::MigrationsController do end end - context 'when user does not have moved to account' do + context 'when user has a moved to account' do let(:moved_to_account) { Fabricate(:account) } it 'renders show page' do @@ -43,21 +44,22 @@ describe Settings::MigrationsController do end end - describe 'PUT #update' do + describe 'POST #create' do context 'when user is not sign in' do - subject { put :update } + subject { post :create } it_behaves_like 'authenticate user' end context 'when user is sign in' do - subject { put :update, params: { migration: { acct: acct } } } + subject { post :create, params: { account_migration: { acct: acct, current_password: '12345678' } } } + + let(:user) { Fabricate(:user, password: '12345678') } - let(:user) { Fabricate(:user) } before { sign_in user, scope: :user } context 'when migration account is changed' do - let(:acct) { Fabricate(:account) } + let(:acct) { Fabricate(:account, also_known_as: [ActivityPub::TagManager.instance.uri_for(user.account)]) } it 'updates moved to account' do is_expected.to redirect_to settings_migration_path diff --git a/spec/fabricators/account_alias_fabricator.rb b/spec/fabricators/account_alias_fabricator.rb new file mode 100644 index 000000000..94dde9bb8 --- /dev/null +++ b/spec/fabricators/account_alias_fabricator.rb @@ -0,0 +1,5 @@ +Fabricator(:account_alias) do + account + acct 'test@example.com' + uri 'https://example.com/users/test' +end diff --git a/spec/fabricators/account_migration_fabricator.rb b/spec/fabricators/account_migration_fabricator.rb new file mode 100644 index 000000000..3b3fc2077 --- /dev/null +++ b/spec/fabricators/account_migration_fabricator.rb @@ -0,0 +1,6 @@ +Fabricator(:account_migration) do + account + target_account + followers_count 1234 + acct 'test@example.com' +end diff --git a/spec/models/account_alias_spec.rb b/spec/models/account_alias_spec.rb new file mode 100644 index 000000000..27ec215aa --- /dev/null +++ b/spec/models/account_alias_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe AccountAlias, type: :model do + +end diff --git a/spec/models/account_migration_spec.rb b/spec/models/account_migration_spec.rb new file mode 100644 index 000000000..8461b4b28 --- /dev/null +++ b/spec/models/account_migration_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe AccountMigration, type: :model do + +end -- cgit From fb45f6d9119fc8d1f24cb44c5a60b65c8700014d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 3 Oct 2019 03:59:43 +0200 Subject: Add br, es-AR, et, mk, nn to available locales (#12062) --- app/helpers/settings_helper.rb | 15 ++++++++++----- config/application.rb | 11 ++++++++--- 2 files changed, 18 insertions(+), 8 deletions(-) (limited to 'app/helpers') diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index ecc73baf5..aa0a4d467 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -2,11 +2,11 @@ module SettingsHelper HUMAN_LOCALES = { - en: 'English', ar: 'العربية', ast: 'Asturianu', bg: 'Български', bn: 'বাংলা', + br: 'Breton', ca: 'Català', co: 'Corsu', cs: 'Čeština', @@ -14,8 +14,11 @@ module SettingsHelper da: 'Dansk', de: 'Deutsch', el: 'Ελληνικά', + en: 'English', eo: 'Esperanto', + 'es-AR': 'Español (Argentina)', es: 'Español', + et: 'Eesti', eu: 'Euskara', fa: 'فارسی', fi: 'Suomi', @@ -36,32 +39,34 @@ module SettingsHelper ko: '한국어', lt: 'Lietuvių', lv: 'Latviešu', + mk: 'Македонски', ml: 'മലയാളം', ms: 'Bahasa Melayu', nl: 'Nederlands', + nn: 'Nynorsk', no: 'Norsk', oc: 'Occitan', pl: 'Polski', - pt: 'Português', - 'pt-PT': 'Português (Portugal)', 'pt-BR': 'Português (Brasil)', + 'pt-PT': 'Português (Portugal)', + pt: 'Português', ro: 'Română', ru: 'Русский', sk: 'Slovenčina', sl: 'Slovenščina', sq: 'Shqip', - sr: 'Српски', 'sr-Latn': 'Srpski (latinica)', + sr: 'Српски', sv: 'Svenska', ta: 'தமிழ்', te: 'తెలుగు', th: 'ไทย', tr: 'Türkçe', uk: 'Українська', - zh: '中文', 'zh-CN': '简体中文', 'zh-HK': '繁體中文(香港)', 'zh-TW': '繁體中文(臺灣)', + zh: '中文', }.freeze def human_locale(locale) diff --git a/config/application.rb b/config/application.rb index 60f73f8bb..9be41b1a7 100644 --- a/config/application.rb +++ b/config/application.rb @@ -39,11 +39,11 @@ module Mastodon # All translations from config/locales/*.rb,yml are auto loaded. # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] config.i18n.available_locales = [ - :en, :ar, :ast, :bg, :bn, + :br, :ca, :co, :cs, @@ -51,8 +51,11 @@ module Mastodon :da, :de, :el, + :en, :eo, + :'es-AR', :es, + :et, :eu, :fa, :fi, @@ -73,20 +76,22 @@ module Mastodon :ko, :lt, :lv, + :mk, :ms, :nl, + :nn, :no, :oc, :pl, - :'pt-PT', :'pt-BR', + :'pt-PT', :ro, :ru, :sk, :sl, :sq, - :sr, :'sr-Latn', + :sr, :sv, :ta, :te, -- cgit From a6269b2f83e3eed1a8ab545f5756cd7b582075f5 Mon Sep 17 00:00:00 2001 From: Takeshi Umeda Date: Fri, 25 Oct 2019 05:50:09 +0900 Subject: Split AccountsHelper from StatusesHelper (#12078) --- app/helpers/accounts_helper.rb | 106 +++++++++++++++++++++ app/helpers/statuses_helper.rb | 103 -------------------- app/mailers/admin_mailer.rb | 2 +- app/mailers/notification_mailer.rb | 1 + app/mailers/user_mailer.rb | 2 +- app/serializers/rss/account_serializer.rb | 2 +- app/serializers/rss/tag_serializer.rb | 1 - spec/helpers/accounts_helper_spec.rb | 67 +++++++++++++ .../admin/account_moderation_notes_helper_spec.rb | 2 +- spec/helpers/statuses_helper_spec.rb | 54 ----------- 10 files changed, 178 insertions(+), 162 deletions(-) create mode 100644 app/helpers/accounts_helper.rb create mode 100644 spec/helpers/accounts_helper_spec.rb (limited to 'app/helpers') diff --git a/app/helpers/accounts_helper.rb b/app/helpers/accounts_helper.rb new file mode 100644 index 000000000..5b060a181 --- /dev/null +++ b/app/helpers/accounts_helper.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +module AccountsHelper + def display_name(account, **options) + if options[:custom_emojify] + Formatter.instance.format_display_name(account, options) + else + account.display_name.presence || account.username + end + end + + def acct(account) + if account.local? + "@#{account.acct}@#{Rails.configuration.x.local_domain}" + else + "@#{account.acct}" + end + end + + def account_action_button(account) + if user_signed_in? + if account.id == current_user.account_id + link_to settings_profile_url, class: 'button logo-button' do + safe_join([svg_logo, t('settings.edit_profile')]) + end + elsif current_account.following?(account) || current_account.requested?(account) + link_to account_unfollow_path(account), class: 'button logo-button button--destructive', data: { method: :post } do + safe_join([svg_logo, t('accounts.unfollow')]) + end + elsif !(account.memorial? || account.moved?) + link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do + safe_join([svg_logo, t('accounts.follow')]) + end + end + elsif !(account.memorial? || account.moved?) + link_to account_remote_follow_path(account), class: 'button logo-button modal-button', target: '_new' do + safe_join([svg_logo, t('accounts.follow')]) + end + end + end + + def minimal_account_action_button(account) + if user_signed_in? + return if account.id == current_user.account_id + + if current_account.following?(account) || current_account.requested?(account) + link_to account_unfollow_path(account), class: 'icon-button active', data: { method: :post }, title: t('accounts.unfollow') do + fa_icon('user-times fw') + end + elsif !(account.memorial? || account.moved?) + link_to account_follow_path(account), class: "icon-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post }, title: t('accounts.follow') do + fa_icon('user-plus fw') + end + end + elsif !(account.memorial? || account.moved?) + link_to account_remote_follow_path(account), class: 'icon-button modal-button', target: '_new', title: t('accounts.follow') do + fa_icon('user-plus fw') + end + end + end + + def account_badge(account, all: false) + if account.bot? + content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles') + elsif (Setting.show_staff_badge && account.user_staff?) || all + content_tag(:div, class: 'roles') do + if all && !account.user_staff? + content_tag(:div, t('admin.accounts.roles.user'), class: 'account-role') + elsif account.user_admin? + content_tag(:div, t('accounts.roles.admin'), class: 'account-role admin') + elsif account.user_moderator? + content_tag(:div, t('accounts.roles.moderator'), class: 'account-role moderator') + end + end + end + end + + def account_description(account) + prepend_str = [ + [ + number_to_human(account.statuses_count, strip_insignificant_zeros: true), + I18n.t('accounts.posts', count: account.statuses_count), + ].join(' '), + + [ + number_to_human(account.following_count, strip_insignificant_zeros: true), + I18n.t('accounts.following', count: account.following_count), + ].join(' '), + + [ + number_to_human(account.followers_count, strip_insignificant_zeros: true), + I18n.t('accounts.followers', count: account.followers_count), + ].join(' '), + ].join(', ') + + [prepend_str, account.note].join(' · ') + end + + def svg_logo + content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976') + end + + def svg_logo_full + content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo-full'), 'viewBox' => '0 0 713.35878 175.8678') + end +end diff --git a/app/helpers/statuses_helper.rb b/app/helpers/statuses_helper.rb index 8380b3c42..866a9902c 100644 --- a/app/helpers/statuses_helper.rb +++ b/app/helpers/statuses_helper.rb @@ -4,80 +4,6 @@ module StatusesHelper EMBEDDED_CONTROLLER = 'statuses' EMBEDDED_ACTION = 'embed' - def display_name(account, **options) - if options[:custom_emojify] - Formatter.instance.format_display_name(account, options) - else - account.display_name.presence || account.username - end - end - - def account_action_button(account) - if user_signed_in? - if account.id == current_user.account_id - link_to settings_profile_url, class: 'button logo-button' do - safe_join([svg_logo, t('settings.edit_profile')]) - end - elsif current_account.following?(account) || current_account.requested?(account) - link_to account_unfollow_path(account), class: 'button logo-button button--destructive', data: { method: :post } do - safe_join([svg_logo, t('accounts.unfollow')]) - end - elsif !(account.memorial? || account.moved?) - link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do - safe_join([svg_logo, t('accounts.follow')]) - end - end - elsif !(account.memorial? || account.moved?) - link_to account_remote_follow_path(account), class: 'button logo-button modal-button', target: '_new' do - safe_join([svg_logo, t('accounts.follow')]) - end - end - end - - def minimal_account_action_button(account) - if user_signed_in? - return if account.id == current_user.account_id - - if current_account.following?(account) || current_account.requested?(account) - link_to account_unfollow_path(account), class: 'icon-button active', data: { method: :post }, title: t('accounts.unfollow') do - fa_icon('user-times fw') - end - elsif !(account.memorial? || account.moved?) - link_to account_follow_path(account), class: "icon-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post }, title: t('accounts.follow') do - fa_icon('user-plus fw') - end - end - elsif !(account.memorial? || account.moved?) - link_to account_remote_follow_path(account), class: 'icon-button modal-button', target: '_new', title: t('accounts.follow') do - fa_icon('user-plus fw') - end - end - end - - def svg_logo - content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976') - end - - def svg_logo_full - content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo-full'), 'viewBox' => '0 0 713.35878 175.8678') - end - - def account_badge(account, all: false) - if account.bot? - content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles') - elsif (Setting.show_staff_badge && account.user_staff?) || all - content_tag(:div, class: 'roles') do - if all && !account.user_staff? - content_tag(:div, t('admin.accounts.roles.user'), class: 'account-role') - elsif account.user_admin? - content_tag(:div, t('accounts.roles.admin'), class: 'account-role admin') - elsif account.user_moderator? - content_tag(:div, t('accounts.roles.moderator'), class: 'account-role moderator') - end - end - end - end - def link_to_more(url) link_to t('statuses.show_more'), url, class: 'load-more load-gap' end @@ -88,27 +14,6 @@ module StatusesHelper end end - def account_description(account) - prepend_str = [ - [ - number_to_human(account.statuses_count, strip_insignificant_zeros: true), - I18n.t('accounts.posts', count: account.statuses_count), - ].join(' '), - - [ - number_to_human(account.following_count, strip_insignificant_zeros: true), - I18n.t('accounts.following', count: account.following_count), - ].join(' '), - - [ - number_to_human(account.followers_count, strip_insignificant_zeros: true), - I18n.t('accounts.followers', count: account.followers_count), - ].join(' '), - ].join(', ') - - [prepend_str, account.note].join(' · ') - end - def media_summary(status) attachments = { image: 0, video: 0 } @@ -154,14 +59,6 @@ module StatusesHelper embedded_view? ? '_blank' : nil end - def acct(account) - if account.local? - "@#{account.acct}@#{Rails.configuration.x.local_domain}" - else - "@#{account.acct}" - end - end - def style_classes(status, is_predecessor, is_successor, include_threads) classes = ['entry'] classes << 'entry-predecessor' if is_predecessor diff --git a/app/mailers/admin_mailer.rb b/app/mailers/admin_mailer.rb index 8abce5f05..11fd09e30 100644 --- a/app/mailers/admin_mailer.rb +++ b/app/mailers/admin_mailer.rb @@ -3,7 +3,7 @@ class AdminMailer < ApplicationMailer layout 'plain_mailer' - helper :statuses + helper :accounts def new_report(recipient, report) @report = report diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index 723d901fc..9d8a7886c 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class NotificationMailer < ApplicationMailer + helper :accounts helper :statuses add_template_helper RoutingHelper diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 6b81f6873..c30bec80b 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -3,9 +3,9 @@ class UserMailer < Devise::Mailer layout 'mailer' + helper :accounts helper :application helper :instance - helper :statuses add_template_helper RoutingHelper diff --git a/app/serializers/rss/account_serializer.rb b/app/serializers/rss/account_serializer.rb index e39b2b372..ee972ff96 100644 --- a/app/serializers/rss/account_serializer.rb +++ b/app/serializers/rss/account_serializer.rb @@ -2,7 +2,7 @@ class RSS::AccountSerializer include ActionView::Helpers::NumberHelper - include StatusesHelper + include AccountsHelper include RoutingHelper def render(account, statuses, tag) diff --git a/app/serializers/rss/tag_serializer.rb b/app/serializers/rss/tag_serializer.rb index 6737fb2c9..ea26189a2 100644 --- a/app/serializers/rss/tag_serializer.rb +++ b/app/serializers/rss/tag_serializer.rb @@ -3,7 +3,6 @@ class RSS::TagSerializer include ActionView::Helpers::NumberHelper include ActionView::Helpers::SanitizeHelper - include StatusesHelper include RoutingHelper def render(tag, statuses) diff --git a/spec/helpers/accounts_helper_spec.rb b/spec/helpers/accounts_helper_spec.rb new file mode 100644 index 000000000..2b35b23b7 --- /dev/null +++ b/spec/helpers/accounts_helper_spec.rb @@ -0,0 +1,67 @@ +require 'rails_helper' + +RSpec.describe AccountsHelper, type: :helper do + def set_not_embedded_view + params[:controller] = "not_#{StatusesHelper::EMBEDDED_CONTROLLER}" + params[:action] = "not_#{StatusesHelper::EMBEDDED_ACTION}" + end + + def set_embedded_view + params[:controller] = StatusesHelper::EMBEDDED_CONTROLLER + params[:action] = StatusesHelper::EMBEDDED_ACTION + end + + describe '#display_name' do + it 'uses the display name when it exists' do + account = Account.new(display_name: "Display", username: "Username") + + expect(helper.display_name(account)).to eq "Display" + end + + it 'uses the username when display name is nil' do + account = Account.new(display_name: nil, username: "Username") + + expect(helper.display_name(account)).to eq "Username" + end + end + + describe '#acct' do + it 'is fully qualified for embedded local accounts' do + allow(Rails.configuration.x).to receive(:local_domain).and_return('local_domain') + set_embedded_view + account = Account.new(domain: nil, username: 'user') + + acct = helper.acct(account) + + expect(acct).to eq '@user@local_domain' + end + + it 'is fully qualified for embedded foreign accounts' do + set_embedded_view + account = Account.new(domain: 'foreign_server.com', username: 'user') + + acct = helper.acct(account) + + expect(acct).to eq '@user@foreign_server.com' + end + + it 'is fully qualified for non embedded foreign accounts' do + set_not_embedded_view + account = Account.new(domain: 'foreign_server.com', username: 'user') + + acct = helper.acct(account) + + expect(acct).to eq '@user@foreign_server.com' + end + + it 'is fully qualified for non embedded local accounts' do + allow(Rails.configuration.x).to receive(:local_domain).and_return('local_domain') + set_not_embedded_view + account = Account.new(domain: nil, username: 'user') + + acct = helper.acct(account) + + expect(acct).to eq '@user@local_domain' + end + end +end diff --git a/spec/helpers/admin/account_moderation_notes_helper_spec.rb b/spec/helpers/admin/account_moderation_notes_helper_spec.rb index ddfe8b46f..622ce8806 100644 --- a/spec/helpers/admin/account_moderation_notes_helper_spec.rb +++ b/spec/helpers/admin/account_moderation_notes_helper_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe Admin::AccountModerationNotesHelper, type: :helper do - include StatusesHelper + include AccountsHelper describe '#admin_account_link_to' do context 'account is nil' do diff --git a/spec/helpers/statuses_helper_spec.rb b/spec/helpers/statuses_helper_spec.rb index 510955a2f..940ff072e 100644 --- a/spec/helpers/statuses_helper_spec.rb +++ b/spec/helpers/statuses_helper_spec.rb @@ -1,20 +1,6 @@ require 'rails_helper' RSpec.describe StatusesHelper, type: :helper do - describe '#display_name' do - it 'uses the display name when it exists' do - account = Account.new(display_name: "Display", username: "Username") - - expect(helper.display_name(account)).to eq "Display" - end - - it 'uses the username when display name is nil' do - account = Account.new(display_name: nil, username: "Username") - - expect(helper.display_name(account)).to eq "Username" - end - end - describe '#stream_link_target' do it 'returns nil if it is not an embedded view' do set_not_embedded_view @@ -29,46 +15,6 @@ RSpec.describe StatusesHelper, type: :helper do end end - describe '#acct' do - it 'is fully qualified for embedded local accounts' do - allow(Rails.configuration.x).to receive(:local_domain).and_return('local_domain') - set_embedded_view - account = Account.new(domain: nil, username: 'user') - - acct = helper.acct(account) - - expect(acct).to eq '@user@local_domain' - end - - it 'is fully qualified for embedded foreign accounts' do - set_embedded_view - account = Account.new(domain: 'foreign_server.com', username: 'user') - - acct = helper.acct(account) - - expect(acct).to eq '@user@foreign_server.com' - end - - it 'is fully qualified for non embedded foreign accounts' do - set_not_embedded_view - account = Account.new(domain: 'foreign_server.com', username: 'user') - - acct = helper.acct(account) - - expect(acct).to eq '@user@foreign_server.com' - end - - it 'is fully qualified for non embedded local accounts' do - allow(Rails.configuration.x).to receive(:local_domain).and_return('local_domain') - set_not_embedded_view - account = Account.new(domain: nil, username: 'user') - - acct = helper.acct(account) - - expect(acct).to eq '@user@local_domain' - end - end - def set_not_embedded_view params[:controller] = "not_#{StatusesHelper::EMBEDDED_CONTROLLER}" params[:action] = "not_#{StatusesHelper::EMBEDDED_ACTION}" -- cgit From a2014830c2a015432702edc0c7a8adef6aaa531c Mon Sep 17 00:00:00 2001 From: ThibG Date: Sun, 10 Nov 2019 23:05:15 +0100 Subject: Fix broken admin audit log in whitelist mode (#12303) --- app/helpers/admin/action_logs_helper.rb | 6 ++++-- config/locales/en.yml | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) (limited to 'app/helpers') diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb index 1daa60774..608a99dd5 100644 --- a/app/helpers/admin/action_logs_helper.rb +++ b/app/helpers/admin/action_logs_helper.rb @@ -44,6 +44,8 @@ module Admin::ActionLogsHelper 'flag' when 'DomainBlock' 'lock' + when 'DomainAllow' + 'plus-circle' when 'EmailDomainBlock' 'envelope' when 'Status' @@ -86,7 +88,7 @@ module Admin::ActionLogsHelper record.shortcode when 'Report' link_to "##{record.id}", admin_report_path(record) - when 'DomainBlock', 'EmailDomainBlock' + when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock' link_to record.domain, "https://#{record.domain}" when 'Status' link_to record.account.acct, ActivityPub::TagManager.instance.url_for(record) @@ -99,7 +101,7 @@ module Admin::ActionLogsHelper case type when 'CustomEmoji' attributes['shortcode'] - when 'DomainBlock', 'EmailDomainBlock' + when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock' link_to attributes['domain'], "https://#{attributes['domain']}" when 'Status' tmp_status = Status.new(attributes.except('reblogs_count', 'favourites_count')) diff --git a/config/locales/en.yml b/config/locales/en.yml index 38fe1a382..531036a63 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -203,10 +203,12 @@ en: confirm_user: "%{name} confirmed e-mail address of user %{target}" create_account_warning: "%{name} sent a warning to %{target}" create_custom_emoji: "%{name} uploaded new emoji %{target}" + create_domain_allow: "%{name} whitelisted domain %{target}" create_domain_block: "%{name} blocked domain %{target}" create_email_domain_block: "%{name} blacklisted e-mail domain %{target}" demote_user: "%{name} demoted user %{target}" destroy_custom_emoji: "%{name} destroyed emoji %{target}" + destroy_domain_allow: "%{name} removed domain %{target} from whitelist" destroy_domain_block: "%{name} unblocked domain %{target}" destroy_email_domain_block: "%{name} whitelisted e-mail domain %{target}" destroy_status: "%{name} removed status by %{target}" -- cgit From 24ea938ce188729913ad4c8c0a33899f9c1e733d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 13 Nov 2019 23:36:41 +0100 Subject: Add kn, mr, ur to available locales (#12379) --- app/helpers/settings_helper.rb | 3 +++ config/application.rb | 8 ++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) (limited to 'app/helpers') diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index aa0a4d467..39eb4180e 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -36,11 +36,13 @@ module SettingsHelper ja: '日本語', ka: 'ქართული', kk: 'Қазақша', + kn: 'ಕನ್ನಡ', ko: '한국어', lt: 'Lietuvių', lv: 'Latviešu', mk: 'Македонски', ml: 'മലയാളം', + mr: 'मराठी', ms: 'Bahasa Melayu', nl: 'Nederlands', nn: 'Nynorsk', @@ -63,6 +65,7 @@ module SettingsHelper th: 'ไทย', tr: 'Türkçe', uk: 'Українська', + ur: 'اُردُو', 'zh-CN': '简体中文', 'zh-HK': '繁體中文(香港)', 'zh-TW': '繁體中文(臺灣)', diff --git a/config/application.rb b/config/application.rb index 9be41b1a7..6cd660ca2 100644 --- a/config/application.rb +++ b/config/application.rb @@ -53,8 +53,8 @@ module Mastodon :el, :en, :eo, - :'es-AR', :es, + :'es-AR', :et, :eu, :fa, @@ -73,10 +73,13 @@ module Mastodon :ja, :ka, :kk, + :kn, :ko, :lt, :lv, :mk, + :ml, + :mr, :ms, :nl, :nn, @@ -90,14 +93,15 @@ module Mastodon :sk, :sl, :sq, - :'sr-Latn', :sr, + :'sr-Latn', :sv, :ta, :te, :th, :tr, :uk, + :ur, :'zh-CN', :'zh-HK', :'zh-TW', -- cgit From 5bc4edd0784ed26ada36405ec2ba78a972822983 Mon Sep 17 00:00:00 2001 From: noiob <8197071+noiob@users.noreply.github.com> Date: Thu, 21 Nov 2019 11:35:39 +0100 Subject: Fix whitelist federation for subdomains (#12435) --- app/helpers/domain_control_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/helpers') diff --git a/app/helpers/domain_control_helper.rb b/app/helpers/domain_control_helper.rb index 067b2c2cd..ac60cad29 100644 --- a/app/helpers/domain_control_helper.rb +++ b/app/helpers/domain_control_helper.rb @@ -6,7 +6,7 @@ module DomainControlHelper domain = begin if uri_or_domain.include?('://') - Addressable::URI.parse(uri_or_domain).domain + Addressable::URI.parse(uri_or_domain).host else uri_or_domain end -- cgit From d8f96028c54bb47e6edddbd936bc8f2301dc9fa3 Mon Sep 17 00:00:00 2001 From: ThibG Date: Sat, 30 Nov 2019 19:53:58 +0100 Subject: Add ability to filter reports by target account domain (#12154) * Add ability to filter reports by target account domain * Reword by_target_domain label --- app/controllers/admin/reports_controller.rb | 3 ++- app/helpers/admin/filter_helper.rb | 2 +- app/models/report_filter.rb | 2 ++ app/views/admin/reports/index.html.haml | 14 ++++++++++++++ config/locales/en.yml | 1 + 5 files changed, 20 insertions(+), 2 deletions(-) (limited to 'app/helpers') diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb index f138376b2..09ce1761c 100644 --- a/app/controllers/admin/reports_controller.rb +++ b/app/controllers/admin/reports_controller.rb @@ -55,7 +55,8 @@ module Admin params.permit( :account_id, :resolved, - :target_account_id + :target_account_id, + :by_target_domain ) end diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb index 8af1683e7..fc4f15985 100644 --- a/app/helpers/admin/filter_helper.rb +++ b/app/helpers/admin/filter_helper.rb @@ -2,7 +2,7 @@ module Admin::FilterHelper ACCOUNT_FILTERS = %i(local remote by_domain active pending silenced suspended username display_name email ip staff).freeze - REPORT_FILTERS = %i(resolved account_id target_account_id).freeze + REPORT_FILTERS = %i(resolved account_id target_account_id by_target_domain).freeze INVITE_FILTER = %i(available expired).freeze CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze TAGS_FILTERS = %i(directory reviewed unreviewed pending_review popular active name).freeze diff --git a/app/models/report_filter.rb b/app/models/report_filter.rb index a392d60c3..abf53cbab 100644 --- a/app/models/report_filter.rb +++ b/app/models/report_filter.rb @@ -19,6 +19,8 @@ class ReportFilter def scope_for(key, value) case key.to_sym + when :by_target_domain + Report.where(target_account: Account.where(domain: value)) when :resolved Report.resolved when :account_id diff --git a/app/views/admin/reports/index.html.haml b/app/views/admin/reports/index.html.haml index bfbd32108..b09472270 100644 --- a/app/views/admin/reports/index.html.haml +++ b/app/views/admin/reports/index.html.haml @@ -8,6 +8,20 @@ %li= filter_link_to t('admin.reports.unresolved'), resolved: nil %li= filter_link_to t('admin.reports.resolved'), resolved: '1' += form_tag admin_reports_url, method: 'GET', class: 'simple_form' do + .fields-group + - Admin::FilterHelper::REPORT_FILTERS.each do |key| + - if params[key].present? + = hidden_field_tag key, params[key] + + - %i(by_target_domain).each do |key| + .input.string.optional + = text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.reports.#{key}") + + .actions + %button= t('admin.accounts.search') + = link_to t('admin.accounts.reset'), admin_reports_path, class: 'button negative' + - @reports.group_by(&:target_account_id).each do |target_account_id, reports| - target_account = reports.first.target_account .report-card diff --git a/config/locales/en.yml b/config/locales/en.yml index 783b7a4f6..e69b3596f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -405,6 +405,7 @@ en: are_you_sure: Are you sure? assign_to_self: Assign to me assigned: Assigned moderator + by_target_domain: Domain of reported account comment: none: None created_at: Reported -- cgit From f43f1e01840cd0bad7a88c90d9ea44b183a2a15d Mon Sep 17 00:00:00 2001 From: Takeshi Umeda Date: Thu, 5 Dec 2019 04:36:33 +0900 Subject: Add basic support for group actors (#12071) * Show badge on group actor in WebUI * Do not notify in case of by following group actor * If you mention group actor, also mention group actor followers * Relax characters that can be used in username (same as Application) * Revert "Relax characters that can be used in username (same as Application)" This reverts commit 7e10a137b878d0db1b5252c52106faef5e09ca4b. * Delete display_name method --- app/helpers/statuses_helper.rb | 68 ++++++++++++++++++++++ .../mastodon/features/account/components/header.js | 11 +++- app/lib/activitypub/activity.rb | 6 +- app/lib/activitypub/tag_manager.rb | 30 ++++++++-- app/models/account.rb | 7 +++ app/serializers/activitypub/actor_serializer.rb | 2 + app/serializers/rest/account_serializer.rb | 2 +- config/locales/en.yml | 1 + 8 files changed, 118 insertions(+), 9 deletions(-) (limited to 'app/helpers') diff --git a/app/helpers/statuses_helper.rb b/app/helpers/statuses_helper.rb index 866a9902c..f0e3df944 100644 --- a/app/helpers/statuses_helper.rb +++ b/app/helpers/statuses_helper.rb @@ -4,6 +4,74 @@ module StatusesHelper EMBEDDED_CONTROLLER = 'statuses' EMBEDDED_ACTION = 'embed' + def account_action_button(account) + if user_signed_in? + if account.id == current_user.account_id + link_to settings_profile_url, class: 'button logo-button' do + safe_join([svg_logo, t('settings.edit_profile')]) + end + elsif current_account.following?(account) || current_account.requested?(account) + link_to account_unfollow_path(account), class: 'button logo-button button--destructive', data: { method: :post } do + safe_join([svg_logo, t('accounts.unfollow')]) + end + elsif !(account.memorial? || account.moved?) + link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do + safe_join([svg_logo, t('accounts.follow')]) + end + end + elsif !(account.memorial? || account.moved?) + link_to account_remote_follow_path(account), class: 'button logo-button modal-button', target: '_new' do + safe_join([svg_logo, t('accounts.follow')]) + end + end + end + + def minimal_account_action_button(account) + if user_signed_in? + return if account.id == current_user.account_id + + if current_account.following?(account) || current_account.requested?(account) + link_to account_unfollow_path(account), class: 'icon-button active', data: { method: :post }, title: t('accounts.unfollow') do + fa_icon('user-times fw') + end + elsif !(account.memorial? || account.moved?) + link_to account_follow_path(account), class: "icon-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post }, title: t('accounts.follow') do + fa_icon('user-plus fw') + end + end + elsif !(account.memorial? || account.moved?) + link_to account_remote_follow_path(account), class: 'icon-button modal-button', target: '_new', title: t('accounts.follow') do + fa_icon('user-plus fw') + end + end + end + + def svg_logo + content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976') + end + + def svg_logo_full + content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo-full'), 'viewBox' => '0 0 713.35878 175.8678') + end + + def account_badge(account, all: false) + if account.bot? + content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles') + elsif account.group? + content_tag(:div, content_tag(:div, t('accounts.roles.group'), class: 'account-role group'), class: 'roles') + elsif (Setting.show_staff_badge && account.user_staff?) || all + content_tag(:div, class: 'roles') do + if all && !account.user_staff? + content_tag(:div, t('admin.accounts.roles.user'), class: 'account-role') + elsif account.user_admin? + content_tag(:div, t('accounts.roles.admin'), class: 'account-role admin') + elsif account.user_moderator? + content_tag(:div, t('accounts.roles.moderator'), class: 'account-role moderator') + end + end + end + end + def link_to_more(url) link_to t('statuses.show_more'), url, class: 'load-more load-gap' end diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index dbb567e85..8bd7f2db5 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -238,9 +238,18 @@ class Header extends ImmutablePureComponent { const content = { __html: account.get('note_emojified') }; const displayNameHtml = { __html: account.get('display_name_html') }; const fields = account.get('fields'); - const badge = account.get('bot') ? (
) : null; const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct'); + let badge; + + if (account.get('bot')) { + badge = (
); + } else if (account.get('group')) { + badge = (
); + } else { + badge = null; + } + return (
diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb index cdd406043..0ca6b92a4 100644 --- a/app/lib/activitypub/activity.rb +++ b/app/lib/activitypub/activity.rb @@ -89,7 +89,7 @@ class ActivityPub::Activity def distribute(status) crawl_links(status) - notify_about_reblog(status) if reblog_of_local_account?(status) + notify_about_reblog(status) if reblog_of_local_account?(status) && !reblog_by_following_group_account?(status) notify_about_mentions(status) # Only continue if the status is supposed to have arrived in real-time. @@ -105,6 +105,10 @@ class ActivityPub::Activity status.reblog? && status.reblog.account.local? end + def reblog_by_following_group_account?(status) + status.reblog? && status.account.group? && status.reblog.account.following?(status.account) + end + def notify_about_reblog(status) NotifyService.new.call(status.reblog.account, status) end diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index 512272dbe..ed680d762 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -68,10 +68,19 @@ class ActivityPub::TagManager if status.account.silenced? # Only notify followers if the account is locally silenced account_ids = status.active_mentions.pluck(:account_id) - to = status.account.followers.where(id: account_ids).map { |account| uri_for(account) } - to.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).map { |request| uri_for(request.account) }) + to = status.account.followers.where(id: account_ids).each_with_object([]) do |account, result| + result << uri_for(account) + result << account.followers_url if account.group? + end + to.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).each_with_object([]) do |request, result| + result << uri_for(request.account) + result << request.account.followers_url if request.account.group? + end) else - status.active_mentions.map { |mention| uri_for(mention.account) } + status.active_mentions.each_with_object([]) do |mention, result| + result << uri_for(mention.account) + result << mention.account.followers_url if mention.account.group? + end end end end @@ -97,10 +106,19 @@ class ActivityPub::TagManager if status.account.silenced? # Only notify followers if the account is locally silenced account_ids = status.active_mentions.pluck(:account_id) - cc.concat(status.account.followers.where(id: account_ids).map { |account| uri_for(account) }) - cc.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).map { |request| uri_for(request.account) }) + cc.concat(status.account.followers.where(id: account_ids).each_with_object([]) do |account, result| + result << uri_for(account) + result << account.followers_url if account.group? + end) + cc.concat(FollowRequest.where(target_account_id: status.account_id, account_id: account_ids).each_with_object([]) do |request, result| + result << uri_for(request.account) + result << request.account.followers_url if request.account.group? + end) else - cc.concat(status.active_mentions.map { |mention| uri_for(mention.account) }) + cc.concat(status.active_mentions.each_with_object([]) do |mention, result| + result << uri_for(mention.account) + result << mention.account.followers_url if mention.account.group? + end) end end diff --git a/app/models/account.rb b/app/models/account.rb index d17782f78..884332e5a 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -93,6 +93,7 @@ class Account < ApplicationRecord scope :without_silenced, -> { where(silenced_at: nil) } scope :recent, -> { reorder(id: :desc) } scope :bots, -> { where(actor_type: %w(Application Service)) } + scope :groups, -> { where(actor_type: 'Group') } scope :alphabetic, -> { order(domain: :asc, username: :asc) } scope :by_domain_accounts, -> { group(:domain).select(:domain, 'COUNT(*) AS accounts_count').order('accounts_count desc') } scope :matches_username, ->(value) { where(arel_table[:username].matches("#{value}%")) } @@ -153,6 +154,12 @@ class Account < ApplicationRecord self.actor_type = ActiveModel::Type::Boolean.new.cast(val) ? 'Service' : 'Person' end + def group? + actor_type == 'Group' + end + + alias group group? + def acct local? ? username : "#{username}@#{domain}" end diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index 17df85de3..aa64936a7 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -49,6 +49,8 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer 'Application' elsif object.bot? 'Service' + elsif object.group? + 'Group' else 'Person' end diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb index 7e3041ae3..5fec75673 100644 --- a/app/serializers/rest/account_serializer.rb +++ b/app/serializers/rest/account_serializer.rb @@ -3,7 +3,7 @@ class REST::AccountSerializer < ActiveModel::Serializer include RoutingHelper - attributes :id, :username, :acct, :display_name, :locked, :bot, :discoverable, :created_at, + attributes :id, :username, :acct, :display_name, :locked, :bot, :discoverable, :group, :created_at, :note, :url, :avatar, :avatar_static, :header, :header_static, :followers_count, :following_count, :statuses_count, :last_status_at diff --git a/config/locales/en.yml b/config/locales/en.yml index d498f6ce3..f6a14ad1a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -78,6 +78,7 @@ en: roles: admin: Admin bot: Bot + group: Group moderator: Mod unavailable: Profile unavailable unfollow: Unfollow -- cgit From 04582e3c3e7db716c32261583a956d889c438254 Mon Sep 17 00:00:00 2001 From: Takeshi Umeda Date: Thu, 5 Dec 2019 08:50:40 +0900 Subject: Remove some duplicate methods from StatusHelper and reflect changes to AccountHelper (#12545) --- app/helpers/accounts_helper.rb | 2 ++ app/helpers/statuses_helper.rb | 68 ------------------------------------------ 2 files changed, 2 insertions(+), 68 deletions(-) (limited to 'app/helpers') diff --git a/app/helpers/accounts_helper.rb b/app/helpers/accounts_helper.rb index 5b060a181..99815be7b 100644 --- a/app/helpers/accounts_helper.rb +++ b/app/helpers/accounts_helper.rb @@ -62,6 +62,8 @@ module AccountsHelper def account_badge(account, all: false) if account.bot? content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles') + elsif account.group? + content_tag(:div, content_tag(:div, t('accounts.roles.group'), class: 'account-role group'), class: 'roles') elsif (Setting.show_staff_badge && account.user_staff?) || all content_tag(:div, class: 'roles') do if all && !account.user_staff? diff --git a/app/helpers/statuses_helper.rb b/app/helpers/statuses_helper.rb index f0e3df944..866a9902c 100644 --- a/app/helpers/statuses_helper.rb +++ b/app/helpers/statuses_helper.rb @@ -4,74 +4,6 @@ module StatusesHelper EMBEDDED_CONTROLLER = 'statuses' EMBEDDED_ACTION = 'embed' - def account_action_button(account) - if user_signed_in? - if account.id == current_user.account_id - link_to settings_profile_url, class: 'button logo-button' do - safe_join([svg_logo, t('settings.edit_profile')]) - end - elsif current_account.following?(account) || current_account.requested?(account) - link_to account_unfollow_path(account), class: 'button logo-button button--destructive', data: { method: :post } do - safe_join([svg_logo, t('accounts.unfollow')]) - end - elsif !(account.memorial? || account.moved?) - link_to account_follow_path(account), class: "button logo-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post } do - safe_join([svg_logo, t('accounts.follow')]) - end - end - elsif !(account.memorial? || account.moved?) - link_to account_remote_follow_path(account), class: 'button logo-button modal-button', target: '_new' do - safe_join([svg_logo, t('accounts.follow')]) - end - end - end - - def minimal_account_action_button(account) - if user_signed_in? - return if account.id == current_user.account_id - - if current_account.following?(account) || current_account.requested?(account) - link_to account_unfollow_path(account), class: 'icon-button active', data: { method: :post }, title: t('accounts.unfollow') do - fa_icon('user-times fw') - end - elsif !(account.memorial? || account.moved?) - link_to account_follow_path(account), class: "icon-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post }, title: t('accounts.follow') do - fa_icon('user-plus fw') - end - end - elsif !(account.memorial? || account.moved?) - link_to account_remote_follow_path(account), class: 'icon-button modal-button', target: '_new', title: t('accounts.follow') do - fa_icon('user-plus fw') - end - end - end - - def svg_logo - content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976') - end - - def svg_logo_full - content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo-full'), 'viewBox' => '0 0 713.35878 175.8678') - end - - def account_badge(account, all: false) - if account.bot? - content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles') - elsif account.group? - content_tag(:div, content_tag(:div, t('accounts.roles.group'), class: 'account-role group'), class: 'roles') - elsif (Setting.show_staff_badge && account.user_staff?) || all - content_tag(:div, class: 'roles') do - if all && !account.user_staff? - content_tag(:div, t('admin.accounts.roles.user'), class: 'account-role') - elsif account.user_admin? - content_tag(:div, t('accounts.roles.admin'), class: 'account-role admin') - elsif account.user_moderator? - content_tag(:div, t('accounts.roles.moderator'), class: 'account-role moderator') - end - end - end - end - def link_to_more(url) link_to t('statuses.show_more'), url, class: 'load-more load-gap' end -- cgit