Merge pull request #2559 from ClearlyClaire/glitch-soc/merge-upstream

Merge upstream changes up to 5dc634796a
pull/2563/head
Claire 2024-01-11 12:08:56 +01:00 committed by GitHub
commit b523188807
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
195 changed files with 1859 additions and 854 deletions

View File

@ -70,7 +70,7 @@ services:
hard: -1 hard: -1
libretranslate: libretranslate:
image: libretranslate/libretranslate:v1.5.2 image: libretranslate/libretranslate:v1.5.3
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- lt-data:/home/libretranslate/.local - lt-data:/home/libretranslate/.local

View File

@ -245,7 +245,7 @@ module.exports = defineConfig({
}, },
// Immutable / Redux / data store // Immutable / Redux / data store
{ {
pattern: '{immutable,react-redux,react-immutable-proptypes,react-immutable-pure-component,reselect}', pattern: '{immutable,@reduxjs/toolkit,react-redux,react-immutable-proptypes,react-immutable-pure-component}',
group: 'external', group: 'external',
position: 'before', position: 'before',
}, },
@ -370,7 +370,14 @@ module.exports = defineConfig({
'@typescript-eslint/consistent-type-exports': 'error', '@typescript-eslint/consistent-type-exports': 'error',
'@typescript-eslint/consistent-type-imports': 'error', '@typescript-eslint/consistent-type-imports': 'error',
"@typescript-eslint/prefer-nullish-coalescing": ['error', { ignorePrimitives: { boolean: true } }], "@typescript-eslint/prefer-nullish-coalescing": ['error', { ignorePrimitives: { boolean: true } }],
"@typescript-eslint/no-restricted-imports": [
"warn",
{
"name": "react-redux",
"importNames": ["useSelector", "useDispatch"],
"message": "Use typed hooks `useAppDispatch` and `useAppSelector` instead."
}
],
'jsdoc/require-jsdoc': 'off', 'jsdoc/require-jsdoc': 'off',
// Those rules set stricter rules for TS files // Those rules set stricter rules for TS files

View File

@ -1,13 +1,13 @@
# This configuration was generated by # This configuration was generated by
# `haml-lint --auto-gen-config` # `haml-lint --auto-gen-config`
# on 2023-12-15 11:02:19 -0500 using Haml-Lint version 0.52.0. # on 2024-01-08 14:02:57 -0500 using Haml-Lint version 0.53.0.
# The point is for the user to remove these configuration records # The point is for the user to remove these configuration records
# one by one as the lints are removed from the code base. # one by one as the lints are removed from the code base.
# Note that changes in the inspected code, or installation of new # Note that changes in the inspected code, or installation of new
# versions of Haml-Lint, may require this file to be generated again. # versions of Haml-Lint, may require this file to be generated again.
linters: linters:
# Offense count: 11 # Offense count: 10
LineLength: LineLength:
exclude: exclude:
- 'app/views/admin/roles/_form.html.haml' - 'app/views/admin/roles/_form.html.haml'
@ -17,5 +17,4 @@ linters:
- 'app/views/settings/applications/_fields.html.haml' - 'app/views/settings/applications/_fields.html.haml'
- 'app/views/settings/imports/index.html.haml' - 'app/views/settings/imports/index.html.haml'
- 'app/views/settings/preferences/appearance/show.html.haml' - 'app/views/settings/preferences/appearance/show.html.haml'
- 'app/views/settings/preferences/notifications/show.html.haml'
- 'app/views/settings/preferences/other/show.html.haml' - 'app/views/settings/preferences/other/show.html.haml'

View File

@ -118,15 +118,10 @@ Rails/UnusedIgnoredColumns:
Rails/NegateInclude: Rails/NegateInclude:
Enabled: false Enabled: false
# Reason: Some single letter camel case files shouldn't be split # Reason: Deprecated cop, will be removed in 3.0, replaced by SpecFilePathFormat
# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecfilepath # https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecfilepath
RSpec/FilePath: RSpec/FilePath:
CustomTransform: Enabled: false
ActivityPub: activitypub
DeepL: deepl
FetchOEmbedService: fetch_oembed_service
OEmbedController: oembed_controller
OStatus: ostatus
# Reason: # Reason:
# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecnamedsubject # https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecnamedsubject

View File

@ -1,6 +1,6 @@
# This configuration was generated by # This configuration was generated by
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp` # `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp`
# using RuboCop version 1.57.2. # using RuboCop version 1.59.0.
# The point is for the user to remove these configuration records # The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base. # one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new # Note that changes in the inspected code, or installation of new
@ -26,7 +26,7 @@ Lint/NonLocalExitFromIterator:
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
Metrics/AbcSize: Metrics/AbcSize:
Max: 100 Max: 90
# Configuration parameters: CountBlocks, Max. # Configuration parameters: CountBlocks, Max.
Metrics/BlockNesting: Metrics/BlockNesting:
@ -50,7 +50,7 @@ RSpec/MultipleExpectations:
# Configuration parameters: AllowSubject. # Configuration parameters: AllowSubject.
RSpec/MultipleMemoizedHelpers: RSpec/MultipleMemoizedHelpers:
Max: 21 Max: 17
# Configuration parameters: AllowedGroups. # Configuration parameters: AllowedGroups.
RSpec/NestedGroups: RSpec/NestedGroups:
@ -66,7 +66,6 @@ Rails/ApplicationController:
Rails/HasAndBelongsToMany: Rails/HasAndBelongsToMany:
Exclude: Exclude:
- 'app/models/concerns/account/associations.rb' - 'app/models/concerns/account/associations.rb'
- 'app/models/preview_card.rb'
- 'app/models/status.rb' - 'app/models/status.rb'
- 'app/models/tag.rb' - 'app/models/tag.rb'
@ -144,7 +143,6 @@ Rails/WhereExists:
Exclude: Exclude:
- 'app/controllers/activitypub/inboxes_controller.rb' - 'app/controllers/activitypub/inboxes_controller.rb'
- 'app/controllers/admin/email_domain_blocks_controller.rb' - 'app/controllers/admin/email_domain_blocks_controller.rb'
- 'app/controllers/auth/registrations_controller.rb'
- 'app/lib/activitypub/activity/create.rb' - 'app/lib/activitypub/activity/create.rb'
- 'app/lib/delivery_failure_tracker.rb' - 'app/lib/delivery_failure_tracker.rb'
- 'app/lib/feed_manager.rb' - 'app/lib/feed_manager.rb'
@ -160,24 +158,16 @@ Rails/WhereExists:
- 'app/serializers/rest/announcement_serializer.rb' - 'app/serializers/rest/announcement_serializer.rb'
- 'app/serializers/rest/tag_serializer.rb' - 'app/serializers/rest/tag_serializer.rb'
- 'app/services/activitypub/fetch_remote_status_service.rb' - 'app/services/activitypub/fetch_remote_status_service.rb'
- 'app/services/app_sign_up_service.rb'
- 'app/services/vote_service.rb' - 'app/services/vote_service.rb'
- 'app/validators/reaction_validator.rb' - 'app/validators/reaction_validator.rb'
- 'app/validators/vote_validator.rb' - 'app/validators/vote_validator.rb'
- 'app/workers/move_worker.rb' - 'app/workers/move_worker.rb'
- 'db/migrate/20190529143559_preserve_old_layout_for_existing_users.rb'
- 'lib/tasks/tests.rake' - 'lib/tasks/tests.rake'
- 'spec/models/account_spec.rb' - 'spec/models/account_spec.rb'
- 'spec/services/activitypub/process_collection_service_spec.rb' - 'spec/services/activitypub/process_collection_service_spec.rb'
- 'spec/services/purge_domain_service_spec.rb' - 'spec/services/purge_domain_service_spec.rb'
- 'spec/services/unallow_domain_service_spec.rb' - 'spec/services/unallow_domain_service_spec.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowOnConstant, AllowOnSelfClass.
Style/CaseEquality:
Exclude:
- 'config/initializers/trusted_proxies.rb'
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: AllowedMethods, AllowedPatterns. # Configuration parameters: AllowedMethods, AllowedPatterns.
# AllowedMethods: ==, equal?, eql? # AllowedMethods: ==, equal?, eql?
@ -205,8 +195,8 @@ Style/FetchEnvVar:
- 'config/initializers/devise.rb' - 'config/initializers/devise.rb'
- 'config/initializers/paperclip.rb' - 'config/initializers/paperclip.rb'
- 'config/initializers/vapid.rb' - 'config/initializers/vapid.rb'
- 'lib/premailer_webpack_strategy.rb'
- 'lib/mastodon/redis_config.rb' - 'lib/mastodon/redis_config.rb'
- 'lib/premailer_webpack_strategy.rb'
- 'lib/tasks/repo.rake' - 'lib/tasks/repo.rake'
- 'spec/features/profile_spec.rb' - 'spec/features/profile_spec.rb'
@ -223,7 +213,6 @@ Style/FormatStringToken:
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
Style/GlobalStdStream: Style/GlobalStdStream:
Exclude: Exclude:
- 'config/boot.rb'
- 'config/environments/development.rb' - 'config/environments/development.rb'
- 'config/environments/production.rb' - 'config/environments/production.rb'
@ -253,8 +242,6 @@ Style/GuardClause:
- 'app/workers/redownload_media_worker.rb' - 'app/workers/redownload_media_worker.rb'
- 'app/workers/remote_account_refresh_worker.rb' - 'app/workers/remote_account_refresh_worker.rb'
- 'config/initializers/devise.rb' - 'config/initializers/devise.rb'
- 'db/migrate/20170901141119_truncate_preview_cards.rb'
- 'db/post_migrate/20220704024901_migrate_settings_to_user_roles.rb'
- 'lib/devise/strategies/two_factor_ldap_authenticatable.rb' - 'lib/devise/strategies/two_factor_ldap_authenticatable.rb'
- 'lib/devise/strategies/two_factor_pam_authenticatable.rb' - 'lib/devise/strategies/two_factor_pam_authenticatable.rb'
- 'lib/mastodon/cli/accounts.rb' - 'lib/mastodon/cli/accounts.rb'
@ -275,7 +262,6 @@ Style/HashAsLastArrayItem:
- 'app/models/status.rb' - 'app/models/status.rb'
- 'app/services/batched_remove_status_service.rb' - 'app/services/batched_remove_status_service.rb'
- 'app/services/notify_service.rb' - 'app/services/notify_service.rb'
- 'db/migrate/20181024224956_migrate_account_conversations.rb'
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
Style/HashTransformValues: Style/HashTransformValues:
@ -415,8 +401,8 @@ Style/TrailingCommaInHashLiteral:
- 'config/environments/test.rb' - 'config/environments/test.rb'
# This cop supports safe autocorrection (--autocorrect). # This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle, MinSize, WordRegex. # Configuration parameters: WordRegex.
# SupportedStyles: percent, brackets # SupportedStyles: percent, brackets
Style/WordArray: Style/WordArray:
Exclude: EnforcedStyle: percent
- 'app/helpers/languages_helper.rb' MinSize: 3

View File

@ -90,7 +90,7 @@ gem 'sidekiq-bulk', '~> 0.2.0'
gem 'simple-navigation', '~> 4.4' gem 'simple-navigation', '~> 4.4'
gem 'simple_form', '~> 5.2' gem 'simple_form', '~> 5.2'
gem 'stoplight', '~> 3.0.1' gem 'stoplight', '~> 3.0.1'
gem 'strong_migrations', '1.6.4' gem 'strong_migrations', '1.7.0'
gem 'tty-prompt', '~> 0.23', require: false gem 'tty-prompt', '~> 0.23', require: false
gem 'twitter-text', '~> 3.1.0' gem 'twitter-text', '~> 3.1.0'
gem 'tzinfo-data', '~> 1.2023' gem 'tzinfo-data', '~> 1.2023'

View File

@ -336,8 +336,8 @@ GEM
activesupport (>= 5.1) activesupport (>= 5.1)
haml (>= 4.0.6) haml (>= 4.0.6)
railties (>= 5.1) railties (>= 5.1)
haml_lint (0.52.0) haml_lint (0.53.0)
haml (>= 4.0) haml (>= 5.0)
parallel (~> 1.10) parallel (~> 1.10)
rainbow rainbow
rubocop (>= 1.0) rubocop (>= 1.0)
@ -467,14 +467,14 @@ GEM
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.3.0) multipart-post (2.3.0)
mutex_m (0.2.0) mutex_m (0.2.0)
net-http (0.4.0) net-http (0.4.1)
uri uri
net-http-persistent (4.0.2) net-http-persistent (4.0.2)
connection_pool (~> 2.2) connection_pool (~> 2.2)
net-imap (0.4.4) net-imap (0.4.4)
date date
net-protocol net-protocol
net-ldap (0.18.0) net-ldap (0.19.0)
net-pop (0.1.2) net-pop (0.1.2)
net-protocol net-protocol
net-protocol (0.2.2) net-protocol (0.2.2)
@ -544,7 +544,7 @@ GEM
psych (5.1.2) psych (5.1.2)
stringio stringio
public_suffix (5.0.4) public_suffix (5.0.4)
puma (6.4.1) puma (6.4.2)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.3.1) pundit (2.3.1)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
@ -676,11 +676,11 @@ GEM
unicode-display_width (>= 2.4.0, < 3.0) unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.30.0) rubocop-ast (1.30.0)
parser (>= 3.2.1.0) parser (>= 3.2.1.0)
rubocop-capybara (2.19.0) rubocop-capybara (2.20.0)
rubocop (~> 1.41) rubocop (~> 1.41)
rubocop-factory_bot (2.24.0) rubocop-factory_bot (2.25.0)
rubocop (~> 1.33) rubocop (~> 1.33)
rubocop-performance (1.20.1) rubocop-performance (1.20.2)
rubocop (>= 1.48.1, < 2.0) rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.30.0, < 2.0) rubocop-ast (>= 1.30.0, < 2.0)
rubocop-rails (2.23.1) rubocop-rails (2.23.1)
@ -688,7 +688,7 @@ GEM
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0) rubocop (>= 1.33.0, < 2.0)
rubocop-ast (>= 1.30.0, < 2.0) rubocop-ast (>= 1.30.0, < 2.0)
rubocop-rspec (2.25.0) rubocop-rspec (2.26.1)
rubocop (~> 1.40) rubocop (~> 1.40)
rubocop-capybara (~> 2.17) rubocop-capybara (~> 2.17)
rubocop-factory_bot (~> 2.22) rubocop-factory_bot (~> 2.22)
@ -748,7 +748,7 @@ GEM
stoplight (3.0.2) stoplight (3.0.2)
redlock (~> 1.0) redlock (~> 1.0)
stringio (3.1.0) stringio (3.1.0)
strong_migrations (1.6.4) strong_migrations (1.7.0)
activerecord (>= 5.2) activerecord (>= 5.2)
swd (1.3.0) swd (1.3.0)
activesupport (>= 3) activesupport (>= 3)
@ -952,7 +952,7 @@ DEPENDENCIES
simplecov-lcov (~> 0.8) simplecov-lcov (~> 0.8)
stackprof stackprof
stoplight (~> 3.0.1) stoplight (~> 3.0.1)
strong_migrations (= 1.6.4) strong_migrations (= 1.7.0)
test-prof test-prof
thor (~> 1.2) thor (~> 1.2)
tty-prompt (~> 0.23) tty-prompt (~> 0.23)

View File

@ -40,7 +40,7 @@ module Admin
(@email_domain_block.other_domains || []).uniq.each do |domain| (@email_domain_block.other_domains || []).uniq.each do |domain|
next if EmailDomainBlock.where(domain: domain).exists? next if EmailDomainBlock.where(domain: domain).exists?
other_email_domain_block = EmailDomainBlock.create!(domain: domain, parent: @email_domain_block) other_email_domain_block = EmailDomainBlock.create!(domain: domain, allow_with_approval: @email_domain_block.allow_with_approval, parent: @email_domain_block)
log_action :create, other_email_domain_block log_action :create, other_email_domain_block
end end
end end
@ -65,7 +65,7 @@ module Admin
end end
def resource_params def resource_params
params.require(:email_domain_block).permit(:domain, other_domains: []) params.require(:email_domain_block).permit(:domain, :allow_with_approval, other_domains: [])
end end
def form_email_domain_block_batch_params def form_email_domain_block_batch_params

View File

@ -55,7 +55,7 @@ class Api::V1::Admin::EmailDomainBlocksController < Api::BaseController
end end
def resource_params def resource_params
params.permit(:domain) params.permit(:domain, :allow_with_approval)
end end
def insert_pagination_headers def insert_pagination_headers

View File

@ -3,150 +3,6 @@
module CacheConcern module CacheConcern
extend ActiveSupport::Concern extend ActiveSupport::Concern
module ActiveRecordCoder
EMPTY_HASH = {}.freeze
class << self
def dump(record)
instances = InstanceTracker.new
serialized_associations = serialize_associations(record, instances)
serialized_records = instances.map { |r| serialize_record(r) }
[serialized_associations, *serialized_records]
end
def load(payload)
instances = InstanceTracker.new
serialized_associations, *serialized_records = payload
serialized_records.each { |attrs| instances.push(deserialize_record(*attrs)) }
deserialize_associations(serialized_associations, instances)
end
private
# Records without associations, or which have already been visited before,
# are serialized by their id alone.
#
# Records with associations are serialized as a two-element array including
# their id and the record's association cache.
#
def serialize_associations(record, instances)
return unless record
if (id = instances.lookup(record))
payload = id
else
payload = instances.push(record)
cached_associations = record.class.reflect_on_all_associations.select do |reflection|
record.association_cached?(reflection.name)
end
unless cached_associations.empty?
serialized_associations = cached_associations.map do |reflection|
association = record.association(reflection.name)
serialized_target = if reflection.collection?
association.target.map { |target_record| serialize_associations(target_record, instances) }
else
serialize_associations(association.target, instances)
end
[reflection.name, serialized_target]
end
payload = [payload, serialized_associations]
end
end
payload
end
def deserialize_associations(payload, instances)
return unless payload
id, associations = payload
record = instances.fetch(id)
associations&.each do |name, serialized_target|
begin
association = record.association(name)
rescue ActiveRecord::AssociationNotFoundError
raise AssociationMissingError, "undefined association: #{name}"
end
target = if association.reflection.collection?
serialized_target.map! { |serialized_record| deserialize_associations(serialized_record, instances) }
else
deserialize_associations(serialized_target, instances)
end
association.target = target
end
record
end
def serialize_record(record)
arguments = [record.class.name, attributes_for_database(record)]
arguments << true if record.new_record?
arguments
end
def attributes_for_database(record)
attributes = record.attributes_for_database
attributes.transform_values! { |attr| attr.is_a?(::ActiveModel::Type::Binary::Data) ? attr.to_s : attr }
attributes
end
def deserialize_record(class_name, attributes_from_database, new_record = false) # rubocop:disable Style/OptionalBooleanParameter
begin
klass = Object.const_get(class_name)
rescue NameError
raise ClassMissingError, "undefined class: #{class_name}"
end
# Ideally we'd like to call `klass.instantiate`, however it doesn't allow to pass
# wether the record was persisted or not.
attributes = klass.attributes_builder.build_from_database(attributes_from_database, EMPTY_HASH)
klass.allocate.init_with_attributes(attributes, new_record)
end
end
class Error < StandardError
end
class ClassMissingError < Error
end
class AssociationMissingError < Error
end
class InstanceTracker
def initialize
@instances = []
@ids = {}.compare_by_identity
end
def map(&block)
@instances.map(&block)
end
def fetch(...)
@instances.fetch(...)
end
def push(instance)
id = @ids[instance] = @instances.size
@instances << instance
id
end
def lookup(instance)
@ids[instance]
end
end
end
class_methods do class_methods do
def vary_by(value, **kwargs) def vary_by(value, **kwargs)
before_action(**kwargs) do |controller| before_action(**kwargs) do |controller|
@ -196,11 +52,7 @@ module CacheConcern
raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation) raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation)
return [] if raw.empty? return [] if raw.empty?
cached_keys_with_value = begin cached_keys_with_value = Rails.cache.read_multi(*raw).transform_keys(&:id)
Rails.cache.read_multi(*raw).transform_keys(&:id).transform_values { |r| ActiveRecordCoder.load(r) }
rescue ActiveRecordCoder::Error
{} # The serialization format may have changed, let's pretend it's a cache miss.
end
uncached_ids = raw.map(&:id) - cached_keys_with_value.keys uncached_ids = raw.map(&:id) - cached_keys_with_value.keys
@ -208,10 +60,7 @@ module CacheConcern
unless uncached_ids.empty? unless uncached_ids.empty?
uncached = klass.where(id: uncached_ids).with_includes.index_by(&:id) uncached = klass.where(id: uncached_ids).with_includes.index_by(&:id)
Rails.cache.write_multi(uncached.values.to_h { |i| [i, i] })
uncached.each_value do |item|
Rails.cache.write(item, ActiveRecordCoder.dump(item))
end
end end
raw.filter_map { |item| cached_keys_with_value[item.id] || uncached[item.id] } raw.filter_map { |item| cached_keys_with_value[item.id] || uncached[item.id] }

View File

@ -21,7 +21,7 @@ module WellKnown
username = username_from_resource username = username_from_resource
@account = begin @account = begin
if username == Rails.configuration.x.local_domain if username == Rails.configuration.x.local_domain || username == Rails.configuration.x.web_domain
Account.representative Account.representative
else else
Account.find_local!(username) Account.find_local!(username)

View File

@ -1,6 +1,7 @@
import { createSelector } from '@reduxjs/toolkit';
import { Map as ImmutableMap } from 'immutable'; import { Map as ImmutableMap } from 'immutable';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { useEmoji } from '../../../actions/emojis'; import { useEmoji } from '../../../actions/emojis';
import { changeSetting } from '../../../actions/settings'; import { changeSetting } from '../../../actions/settings';

View File

@ -1,6 +1,7 @@
import { createSelector } from '@reduxjs/toolkit';
import { Map as ImmutableMap } from 'immutable'; import { Map as ImmutableMap } from 'immutable';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { changeComposeLanguage } from 'flavours/glitch/actions/compose'; import { changeComposeLanguage } from 'flavours/glitch/actions/compose';
import { useLanguage } from 'flavours/glitch/actions/languages'; import { useLanguage } from 'flavours/glitch/actions/languages';

View File

@ -1,6 +1,7 @@
import { createSelector } from '@reduxjs/toolkit';
import { Map as ImmutableMap } from 'immutable'; import { Map as ImmutableMap } from 'immutable';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { addReaction, removeReaction, dismissAnnouncement } from 'flavours/glitch/actions/announcements'; import { addReaction, removeReaction, dismissAnnouncement } from 'flavours/glitch/actions/announcements';

View File

@ -4,11 +4,11 @@ import { defineMessages, injectIntl } from 'react-intl';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { createSelector } from '@reduxjs/toolkit';
import { List as ImmutableList } from 'immutable'; import { List as ImmutableList } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchFollowRequests } from 'flavours/glitch/actions/accounts'; import { fetchFollowRequests } from 'flavours/glitch/actions/accounts';
import { fetchLists } from 'flavours/glitch/actions/lists'; import { fetchLists } from 'flavours/glitch/actions/lists';

View File

@ -6,9 +6,9 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { createSelector } from '@reduxjs/toolkit';
import { List as ImmutableList } from 'immutable'; import { List as ImmutableList } from 'immutable';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchAnnouncements, toggleShowAnnouncements } from 'flavours/glitch/actions/announcements'; import { fetchAnnouncements, toggleShowAnnouncements } from 'flavours/glitch/actions/announcements';
import { IconWithBadge } from 'flavours/glitch/components/icon_with_badge'; import { IconWithBadge } from 'flavours/glitch/components/icon_with_badge';

View File

@ -2,10 +2,10 @@ import PropTypes from 'prop-types';
import { injectIntl } from 'react-intl'; import { injectIntl } from 'react-intl';
import { createSelector } from '@reduxjs/toolkit';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { setupListAdder, resetListAdder } from '../../actions/lists'; import { setupListAdder, resetListAdder } from '../../actions/lists';
import NewListForm from '../lists/components/new_list_form'; import NewListForm from '../lists/components/new_list_form';

View File

@ -4,10 +4,10 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { createSelector } from '@reduxjs/toolkit';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchLists } from 'flavours/glitch/actions/lists'; import { fetchLists } from 'flavours/glitch/actions/lists';
import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim'; import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim';

View File

@ -6,10 +6,10 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { createSelector } from '@reduxjs/toolkit';
import { List as ImmutableList } from 'immutable'; import { List as ImmutableList } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { debounce } from 'lodash'; import { debounce } from 'lodash';

View File

@ -3,10 +3,10 @@ import { useCallback, useEffect, useRef } from 'react';
import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
import { createSelector } from '@reduxjs/toolkit';
import { OrderedSet, List as ImmutableList } from 'immutable'; import { OrderedSet, List as ImmutableList } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { shallowEqual } from 'react-redux'; import { shallowEqual } from 'react-redux';
import { createSelector } from 'reselect';
import Toggle from 'react-toggle'; import Toggle from 'react-toggle';

View File

@ -6,11 +6,11 @@ import classNames from 'classnames';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { createSelector } from '@reduxjs/toolkit';
import Immutable from 'immutable'; import Immutable from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { HotKeys } from 'react-hotkeys'; import { HotKeys } from 'react-hotkeys';
@ -610,7 +610,7 @@ class Status extends ImmutablePureComponent {
this.setState({ isExpanded: value }); this.setState({ isExpanded: value });
}; };
setRef = c => { setContainerRef = c => {
this.node = c; this.node = c;
}; };
@ -618,12 +618,16 @@ class Status extends ImmutablePureComponent {
this.column = c; this.column = c;
}; };
setStatusRef = c => {
this.statusNode = c;
};
_scrollStatusIntoView () { _scrollStatusIntoView () {
const { status, multiColumn } = this.props; const { status, multiColumn } = this.props;
if (status) { if (status) {
window.requestAnimationFrame(() => { requestIdleCallback(() => {
this.node?.querySelector('.detailed-status__wrapper')?.scrollIntoView(true); this.statusNode?.scrollIntoView(true);
// In the single-column interface, `scrollIntoView` will put the post behind the header, // In the single-column interface, `scrollIntoView` will put the post behind the header,
// so compensate for that. // so compensate for that.
@ -661,9 +665,8 @@ class Status extends ImmutablePureComponent {
} }
// Scroll to focused post if it is loaded // Scroll to focused post if it is loaded
const child = this.node?.querySelector('.detailed-status__wrapper'); if (this.statusNode) {
if (child) { return [0, this.statusNode.offsetTop];
return [0, child.offsetTop];
} }
// Do not scroll otherwise, `componentDidUpdate` will take care of that // Do not scroll otherwise, `componentDidUpdate` will take care of that
@ -730,11 +733,11 @@ class Status extends ImmutablePureComponent {
/> />
<ScrollContainer scrollKey='thread' shouldUpdateScroll={this.shouldUpdateScroll}> <ScrollContainer scrollKey='thread' shouldUpdateScroll={this.shouldUpdateScroll}>
<div className={classNames('scrollable', { fullscreen })} ref={this.setRef}> <div className={classNames('scrollable', { fullscreen })} ref={this.setContainerRef}>
{ancestors} {ancestors}
<HotKeys handlers={handlers}> <HotKeys handlers={handlers}>
<div className={classNames('focusable', 'detailed-status__wrapper', `detailed-status__wrapper-${status.get('visibility')}`)} tabIndex={0} aria-label={textForScreenReader(intl, status, false, isExpanded)}> <div className={classNames('focusable', 'detailed-status__wrapper', `detailed-status__wrapper-${status.get('visibility')}`)} tabIndex={0} aria-label={textForScreenReader(intl, status, false, isExpanded)} ref={this.setStatusRef}>
<DetailedStatus <DetailedStatus
key={`details-${status.get('id')}`} key={`details-${status.get('id')}`}
status={status} status={status}

View File

@ -2,11 +2,11 @@ import PropTypes from 'prop-types';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import { createSelector } from '@reduxjs/toolkit';
import { is, List as ImmutableList, Set as ImmutableSet } from 'immutable'; import { is, List as ImmutableList, Set as ImmutableSet } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { followAccount } from 'flavours/glitch/actions/accounts'; import { followAccount } from 'flavours/glitch/actions/accounts';
import { Button } from 'flavours/glitch/components/button'; import { Button } from 'flavours/glitch/components/button';

View File

@ -1,9 +1,9 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { createSelector } from '@reduxjs/toolkit';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchLists } from 'flavours/glitch/actions/lists'; import { fetchLists } from 'flavours/glitch/actions/lists';

View File

@ -1,6 +1,6 @@
import { createSelector } from '@reduxjs/toolkit';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { debounce } from 'lodash'; import { debounce } from 'lodash';

View File

@ -1,7 +1,6 @@
import type { Reducer } from '@reduxjs/toolkit';
import { Map as ImmutableMap } from 'immutable'; import { Map as ImmutableMap } from 'immutable';
import type { Reducer } from 'redux';
import { import {
followAccountSuccess, followAccountSuccess,
unfollowAccountSuccess, unfollowAccountSuccess,

View File

@ -1,6 +1,5 @@
import { Record as ImmutableRecord, Stack } from 'immutable';
import type { Reducer } from '@reduxjs/toolkit'; import type { Reducer } from '@reduxjs/toolkit';
import { Record as ImmutableRecord, Stack } from 'immutable';
import { COMPOSE_UPLOAD_CHANGE_SUCCESS } from '../actions/compose'; import { COMPOSE_UPLOAD_CHANGE_SUCCESS } from '../actions/compose';
import type { ModalType } from '../actions/modal'; import type { ModalType } from '../actions/modal';

View File

@ -1,7 +1,6 @@
import { Map as ImmutableMap } from 'immutable';
import { isFulfilled } from '@reduxjs/toolkit'; import { isFulfilled } from '@reduxjs/toolkit';
import type { Reducer } from 'redux'; import type { Reducer } from '@reduxjs/toolkit';
import { Map as ImmutableMap } from 'immutable';
import type { ApiRelationshipJSON } from 'flavours/glitch/api_types/relationships'; import type { ApiRelationshipJSON } from 'flavours/glitch/api_types/relationships';
import type { Account } from 'flavours/glitch/models/account'; import type { Account } from 'flavours/glitch/models/account';

View File

@ -1,5 +1,5 @@
import { createSelector } from '@reduxjs/toolkit';
import { Record as ImmutableRecord } from 'immutable'; import { Record as ImmutableRecord } from 'immutable';
import { createSelector } from 'reselect';
import { accountDefaultValues } from 'flavours/glitch/models/account'; import { accountDefaultValues } from 'flavours/glitch/models/account';
import type { Account, AccountShape } from 'flavours/glitch/models/account'; import type { Account, AccountShape } from 'flavours/glitch/models/account';

View File

@ -1,5 +1,5 @@
import { createSelector } from '@reduxjs/toolkit';
import { List as ImmutableList, Map as ImmutableMap } from 'immutable'; import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import { createSelector } from 'reselect';
import { toServerSideType } from 'flavours/glitch/utils/filters'; import { toServerSideType } from 'flavours/glitch/utils/filters';

View File

@ -1,21 +1,35 @@
import type { AnyAction, Middleware } from 'redux'; import { isAction } from '@reduxjs/toolkit';
import type { Action, Middleware } from '@reduxjs/toolkit';
import type { RootState } from '..'; import type { RootState } from '..';
import { showAlertForError } from '../../actions/alerts'; import { showAlertForError } from '../../actions/alerts';
const defaultFailSuffix = 'FAIL'; const defaultFailSuffix = 'FAIL';
const isFailedAction = new RegExp(`${defaultFailSuffix}$`, 'g');
export const errorsMiddleware: Middleware<unknown, RootState> = interface ActionWithMaybeAlertParams extends Action {
skipAlert?: boolean;
skipNotFound?: boolean;
error?: unknown;
}
function isActionWithmaybeAlertParams(
action: unknown,
): action is ActionWithMaybeAlertParams {
return isAction(action);
}
export const errorsMiddleware: Middleware<Record<string, never>, RootState> =
({ dispatch }) => ({ dispatch }) =>
(next) => (next) =>
(action: AnyAction & { skipAlert?: boolean; skipNotFound?: boolean }) => { (action) => {
if (action.type && !action.skipAlert) { if (
const isFail = new RegExp(`${defaultFailSuffix}$`, 'g'); isActionWithmaybeAlertParams(action) &&
!action.skipAlert &&
if (typeof action.type === 'string' && action.type.match(isFail)) { action.type.match(isFailedAction)
) {
dispatch(showAlertForError(action.error, action.skipNotFound)); dispatch(showAlertForError(action.error, action.skipNotFound));
} }
}
return next(action); return next(action);
}; };

View File

@ -3,9 +3,11 @@ import {
isPending as isThunkActionPending, isPending as isThunkActionPending,
isFulfilled as isThunkActionFulfilled, isFulfilled as isThunkActionFulfilled,
isRejected as isThunkActionRejected, isRejected as isThunkActionRejected,
isAction,
} from '@reduxjs/toolkit'; } from '@reduxjs/toolkit';
import type { Middleware, UnknownAction } from '@reduxjs/toolkit';
import { showLoading, hideLoading } from 'react-redux-loading-bar'; import { showLoading, hideLoading } from 'react-redux-loading-bar';
import type { AnyAction, Middleware } from 'redux';
import type { RootState } from '..'; import type { RootState } from '..';
@ -19,14 +21,28 @@ const defaultTypeSuffixes: Config['promiseTypeSuffixes'] = [
'REJECTED', 'REJECTED',
]; ];
interface ActionWithSkipLoading extends UnknownAction {
skipLoading: boolean;
}
function isActionWithSkipLoading(
action: unknown,
): action is ActionWithSkipLoading {
return (
isAction(action) &&
'skipLoading' in action &&
typeof action.skipLoading === 'boolean'
);
}
export const loadingBarMiddleware = ( export const loadingBarMiddleware = (
config: Config = {}, config: Config = {},
): Middleware<unknown, RootState> => { ): Middleware<{ skipLoading?: boolean }, RootState> => {
const promiseTypeSuffixes = config.promiseTypeSuffixes ?? defaultTypeSuffixes; const promiseTypeSuffixes = config.promiseTypeSuffixes ?? defaultTypeSuffixes;
return ({ dispatch }) => return ({ dispatch }) =>
(next) => (next) =>
(action: AnyAction) => { (action) => {
let isPending = false; let isPending = false;
let isFulfilled = false; let isFulfilled = false;
let isRejected = false; let isRejected = false;
@ -39,7 +55,7 @@ export const loadingBarMiddleware = (
else if (isThunkActionFulfilled(action)) isFulfilled = true; else if (isThunkActionFulfilled(action)) isFulfilled = true;
else if (isThunkActionRejected(action)) isRejected = true; else if (isThunkActionRejected(action)) isRejected = true;
} else if ( } else if (
action.type && isActionWithSkipLoading(action) &&
!action.skipLoading && !action.skipLoading &&
typeof action.type === 'string' typeof action.type === 'string'
) { ) {

View File

@ -1,4 +1,5 @@
import type { Middleware, AnyAction } from 'redux'; import { isAction } from '@reduxjs/toolkit';
import type { Middleware, UnknownAction } from '@reduxjs/toolkit';
import ready from 'flavours/glitch/ready'; import ready from 'flavours/glitch/ready';
import { assetHost } from 'flavours/glitch/utils/config'; import { assetHost } from 'flavours/glitch/utils/config';
@ -10,6 +11,21 @@ interface AudioSource {
type: string; type: string;
} }
interface ActionWithMetaSound extends UnknownAction {
meta: { sound: string };
}
function isActionWithMetaSound(action: unknown): action is ActionWithMetaSound {
return (
isAction(action) &&
'meta' in action &&
typeof action.meta === 'object' &&
!!action.meta &&
'sound' in action.meta &&
typeof action.meta.sound === 'string'
);
}
const createAudio = (sources: AudioSource[]) => { const createAudio = (sources: AudioSource[]) => {
const audio = new Audio(); const audio = new Audio();
sources.forEach(({ type, src }) => { sources.forEach(({ type, src }) => {
@ -34,7 +50,10 @@ const play = (audio: HTMLAudioElement) => {
void audio.play(); void audio.play();
}; };
export const soundsMiddleware = (): Middleware<unknown, RootState> => { export const soundsMiddleware = (): Middleware<
Record<string, never>,
RootState
> => {
const soundCache: Record<string, HTMLAudioElement> = {}; const soundCache: Record<string, HTMLAudioElement> = {};
void ready(() => { void ready(() => {
@ -50,14 +69,14 @@ export const soundsMiddleware = (): Middleware<unknown, RootState> => {
]); ]);
}); });
return () => return () => (next) => (action) => {
(next) => if (isActionWithMetaSound(action)) {
(action: AnyAction & { meta?: { sound?: string } }) => { const sound = action.meta.sound;
const sound = action.meta?.sound;
if (sound && Object.hasOwn(soundCache, sound)) { if (sound && Object.hasOwn(soundCache, sound)) {
play(soundCache[sound]); play(soundCache[sound]);
} }
}
return next(action); return next(action);
}; };

View File

@ -1,7 +1,7 @@
import type { TypedUseSelectorHook } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { createAsyncThunk } from '@reduxjs/toolkit'; import { createAsyncThunk } from '@reduxjs/toolkit';
import type { TypedUseSelectorHook } from 'react-redux';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { useDispatch, useSelector } from 'react-redux';
import type { AppDispatch, RootState } from './store'; import type { AppDispatch, RootState } from './store';

View File

@ -1,6 +1,7 @@
import { createSelector } from '@reduxjs/toolkit';
import { Map as ImmutableMap } from 'immutable'; import { Map as ImmutableMap } from 'immutable';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { useEmoji } from '../../../actions/emojis'; import { useEmoji } from '../../../actions/emojis';
import { changeSetting } from '../../../actions/settings'; import { changeSetting } from '../../../actions/settings';

View File

@ -1,6 +1,7 @@
import { createSelector } from '@reduxjs/toolkit';
import { Map as ImmutableMap } from 'immutable'; import { Map as ImmutableMap } from 'immutable';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { changeComposeLanguage } from 'mastodon/actions/compose'; import { changeComposeLanguage } from 'mastodon/actions/compose';
import { useLanguage } from 'mastodon/actions/languages'; import { useLanguage } from 'mastodon/actions/languages';

View File

@ -1,6 +1,7 @@
import { createSelector } from '@reduxjs/toolkit';
import { Map as ImmutableMap } from 'immutable'; import { Map as ImmutableMap } from 'immutable';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { addReaction, removeReaction, dismissAnnouncement } from 'mastodon/actions/announcements'; import { addReaction, removeReaction, dismissAnnouncement } from 'mastodon/actions/announcements';

View File

@ -6,9 +6,9 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { createSelector } from '@reduxjs/toolkit';
import { List as ImmutableList } from 'immutable'; import { List as ImmutableList } from 'immutable';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { ReactComponent as CampaignIcon } from '@material-symbols/svg-600/outlined/campaign.svg'; import { ReactComponent as CampaignIcon } from '@material-symbols/svg-600/outlined/campaign.svg';
import { ReactComponent as HomeIcon } from '@material-symbols/svg-600/outlined/home-fill.svg'; import { ReactComponent as HomeIcon } from '@material-symbols/svg-600/outlined/home-fill.svg';

View File

@ -2,10 +2,10 @@ import PropTypes from 'prop-types';
import { injectIntl } from 'react-intl'; import { injectIntl } from 'react-intl';
import { createSelector } from '@reduxjs/toolkit';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { setupListAdder, resetListAdder } from '../../actions/lists'; import { setupListAdder, resetListAdder } from '../../actions/lists';
import NewListForm from '../lists/components/new_list_form'; import NewListForm from '../lists/components/new_list_form';

View File

@ -4,10 +4,10 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { createSelector } from '@reduxjs/toolkit';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { ReactComponent as ListAltIcon } from '@material-symbols/svg-600/outlined/list_alt.svg'; import { ReactComponent as ListAltIcon } from '@material-symbols/svg-600/outlined/list_alt.svg';

View File

@ -5,10 +5,10 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { createSelector } from '@reduxjs/toolkit';
import { List as ImmutableList } from 'immutable'; import { List as ImmutableList } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { ReactComponent as DoneAllIcon } from '@material-symbols/svg-600/outlined/done_all.svg'; import { ReactComponent as DoneAllIcon } from '@material-symbols/svg-600/outlined/done_all.svg';
import { ReactComponent as NotificationsIcon } from '@material-symbols/svg-600/outlined/notifications-fill.svg'; import { ReactComponent as NotificationsIcon } from '@material-symbols/svg-600/outlined/notifications-fill.svg';

View File

@ -3,10 +3,10 @@ import { useCallback, useEffect, useRef } from 'react';
import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
import { createSelector } from '@reduxjs/toolkit';
import { OrderedSet, List as ImmutableList } from 'immutable'; import { OrderedSet, List as ImmutableList } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { shallowEqual } from 'react-redux'; import { shallowEqual } from 'react-redux';
import { createSelector } from 'reselect';
import Toggle from 'react-toggle'; import Toggle from 'react-toggle';

View File

@ -6,11 +6,11 @@ import classNames from 'classnames';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { createSelector } from '@reduxjs/toolkit';
import Immutable from 'immutable'; import Immutable from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { ReactComponent as VisibilityIcon } from '@material-symbols/svg-600/outlined/visibility.svg'; import { ReactComponent as VisibilityIcon } from '@material-symbols/svg-600/outlined/visibility.svg';
import { ReactComponent as VisibilityOffIcon } from '@material-symbols/svg-600/outlined/visibility_off.svg'; import { ReactComponent as VisibilityOffIcon } from '@material-symbols/svg-600/outlined/visibility_off.svg';
@ -582,16 +582,20 @@ class Status extends ImmutablePureComponent {
)); ));
} }
setRef = c => { setContainerRef = c => {
this.node = c; this.node = c;
}; };
setStatusRef = c => {
this.statusNode = c;
};
_scrollStatusIntoView () { _scrollStatusIntoView () {
const { status, multiColumn } = this.props; const { status, multiColumn } = this.props;
if (status) { if (status) {
window.requestAnimationFrame(() => { requestIdleCallback(() => {
this.node?.querySelector('.detailed-status__wrapper')?.scrollIntoView(true); this.statusNode?.scrollIntoView(true);
// In the single-column interface, `scrollIntoView` will put the post behind the header, // In the single-column interface, `scrollIntoView` will put the post behind the header,
// so compensate for that. // so compensate for that.
@ -629,9 +633,8 @@ class Status extends ImmutablePureComponent {
} }
// Scroll to focused post if it is loaded // Scroll to focused post if it is loaded
const child = this.node?.querySelector('.detailed-status__wrapper'); if (this.statusNode) {
if (child) { return [0, this.statusNode.offsetTop];
return [0, child.offsetTop];
} }
// Do not scroll otherwise, `componentDidUpdate` will take care of that // Do not scroll otherwise, `componentDidUpdate` will take care of that
@ -692,11 +695,11 @@ class Status extends ImmutablePureComponent {
/> />
<ScrollContainer scrollKey='thread' shouldUpdateScroll={this.shouldUpdateScroll}> <ScrollContainer scrollKey='thread' shouldUpdateScroll={this.shouldUpdateScroll}>
<div className={classNames('scrollable', { fullscreen })} ref={this.setRef}> <div className={classNames('scrollable', { fullscreen })} ref={this.setContainerRef}>
{ancestors} {ancestors}
<HotKeys handlers={handlers}> <HotKeys handlers={handlers}>
<div className={classNames('focusable', 'detailed-status__wrapper', `detailed-status__wrapper-${status.get('visibility')}`)} tabIndex={0} aria-label={textForScreenReader(intl, status, false)}> <div className={classNames('focusable', 'detailed-status__wrapper', `detailed-status__wrapper-${status.get('visibility')}`)} tabIndex={0} aria-label={textForScreenReader(intl, status, false)} ref={this.setStatusRef}>
<DetailedStatus <DetailedStatus
key={`details-${status.get('id')}`} key={`details-${status.get('id')}`}
status={status} status={status}

View File

@ -2,11 +2,11 @@ import PropTypes from 'prop-types';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import { createSelector } from '@reduxjs/toolkit';
import { is, List as ImmutableList, Set as ImmutableSet } from 'immutable'; import { is, List as ImmutableList, Set as ImmutableSet } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { ReactComponent as CloseIcon } from '@material-symbols/svg-600/outlined/close.svg'; import { ReactComponent as CloseIcon } from '@material-symbols/svg-600/outlined/close.svg';

View File

@ -1,9 +1,9 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { createSelector } from '@reduxjs/toolkit';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { ReactComponent as ListAltIcon } from '@material-symbols/svg-600/outlined/list_alt.svg'; import { ReactComponent as ListAltIcon } from '@material-symbols/svg-600/outlined/list_alt.svg';

View File

@ -1,6 +1,6 @@
import { createSelector } from '@reduxjs/toolkit';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { debounce } from 'lodash'; import { debounce } from 'lodash';

View File

@ -21,6 +21,7 @@
"account.blocked": "Stanket", "account.blocked": "Stanket",
"account.browse_more_on_origin_server": "Furchal pelloc'h war ar profil orin", "account.browse_more_on_origin_server": "Furchal pelloc'h war ar profil orin",
"account.cancel_follow_request": "Nullañ ar reked heuliañ", "account.cancel_follow_request": "Nullañ ar reked heuliañ",
"account.copy": "Eilañ al liamm war-zu ho profil",
"account.disable_notifications": "Paouez d'am c'hemenn pa vez embannet traoù gant @{name}", "account.disable_notifications": "Paouez d'am c'hemenn pa vez embannet traoù gant @{name}",
"account.domain_blocked": "Domani stanket", "account.domain_blocked": "Domani stanket",
"account.edit_profile": "Kemmañ ar profil", "account.edit_profile": "Kemmañ ar profil",
@ -28,8 +29,9 @@
"account.endorse": "Lakaat war-wel war ar profil", "account.endorse": "Lakaat war-wel war ar profil",
"account.featured_tags.last_status_at": "Toud diwezhañ : {date}", "account.featured_tags.last_status_at": "Toud diwezhañ : {date}",
"account.featured_tags.last_status_never": "Toud ebet", "account.featured_tags.last_status_never": "Toud ebet",
"account.featured_tags.title": "Penngerioù-klik {name}", "account.featured_tags.title": "Hashtagoù pennañ {name}",
"account.follow": "Heuliañ", "account.follow": "Heuliañ",
"account.follow_back": "Heuliañ d'ho tro",
"account.followers": "Tud koumanantet", "account.followers": "Tud koumanantet",
"account.followers.empty": "Den na heul an implijer·ez-mañ c'hoazh.", "account.followers.empty": "Den na heul an implijer·ez-mañ c'hoazh.",
"account.followers_counter": "{count, plural, other{{counter} Heulier·ez}}", "account.followers_counter": "{count, plural, other{{counter} Heulier·ez}}",
@ -38,6 +40,7 @@
"account.follows.empty": "An implijer·ez-mañ na heul den ebet.", "account.follows.empty": "An implijer·ez-mañ na heul den ebet.",
"account.go_to_profile": "Gwelet ar profil", "account.go_to_profile": "Gwelet ar profil",
"account.hide_reblogs": "Kuzh skignadennoù gant @{name}", "account.hide_reblogs": "Kuzh skignadennoù gant @{name}",
"account.in_memoriam": "E koun.",
"account.joined_short": "Amañ abaoe", "account.joined_short": "Amañ abaoe",
"account.languages": "Cheñch ar yezhoù koumanantet", "account.languages": "Cheñch ar yezhoù koumanantet",
"account.link_verified_on": "Gwiriet eo bet perc'hennidigezh al liamm d'an deiziad-mañ : {date}", "account.link_verified_on": "Gwiriet eo bet perc'hennidigezh al liamm d'an deiziad-mañ : {date}",
@ -49,11 +52,13 @@
"account.mute_notifications_short": "Kuzhat ar c'hemennoù", "account.mute_notifications_short": "Kuzhat ar c'hemennoù",
"account.mute_short": "Kuzhat", "account.mute_short": "Kuzhat",
"account.muted": "Kuzhet", "account.muted": "Kuzhet",
"account.no_bio": "Deskrivadur ebet da gaout.",
"account.open_original_page": "Digeriñ ar bajenn orin", "account.open_original_page": "Digeriñ ar bajenn orin",
"account.posts": "Toudoù", "account.posts": "Embannadurioù",
"account.posts_with_replies": "Toudoù ha respontoù", "account.posts_with_replies": "Embannadurioù ha respontoù",
"account.report": "Disklêriañ @{name}", "account.report": "Disklêriañ @{name}",
"account.requested": "O c'hortoz an asant. Klikit evit nullañ ar goulenn heuliañ", "account.requested": "O c'hortoz an asant. Klikit evit nullañ ar goulenn heuliañ",
"account.requested_follow": "Gant {name} eo bet goulennet ho heuliañ",
"account.share": "Skignañ profil @{name}", "account.share": "Skignañ profil @{name}",
"account.show_reblogs": "Diskouez skignadennoù @{name}", "account.show_reblogs": "Diskouez skignadennoù @{name}",
"account.statuses_counter": "{count, plural, one {{counter} Toud} two {{counter} Doud} other {{counter} a Doudoù}}", "account.statuses_counter": "{count, plural, one {{counter} Toud} two {{counter} Doud} other {{counter} a Doudoù}}",
@ -102,6 +107,7 @@
"column.community": "Red-amzer lec'hel", "column.community": "Red-amzer lec'hel",
"column.directory": "Mont a-dreuz ar profiloù", "column.directory": "Mont a-dreuz ar profiloù",
"column.domain_blocks": "Domani berzet", "column.domain_blocks": "Domani berzet",
"column.favourites": "Muiañ-karet",
"column.follow_requests": "Rekedoù heuliañ", "column.follow_requests": "Rekedoù heuliañ",
"column.home": "Degemer", "column.home": "Degemer",
"column.lists": "Listennoù", "column.lists": "Listennoù",
@ -122,6 +128,9 @@
"community.column_settings.remote_only": "Nemet a-bell", "community.column_settings.remote_only": "Nemet a-bell",
"compose.language.change": "Cheñch yezh", "compose.language.change": "Cheñch yezh",
"compose.language.search": "Klask yezhoù...", "compose.language.search": "Klask yezhoù...",
"compose.published.body": "Embannet.",
"compose.published.open": "Digeriñ",
"compose.saved.body": "Enrollet.",
"compose_form.direct_message_warning_learn_more": "Gouzout hiroc'h", "compose_form.direct_message_warning_learn_more": "Gouzout hiroc'h",
"compose_form.encryption_warning": "Toudoù war Mastodon na vezont ket sifret penn-da-benn. Na rannit ket titouroù kizidik dre Mastodon.", "compose_form.encryption_warning": "Toudoù war Mastodon na vezont ket sifret penn-da-benn. Na rannit ket titouroù kizidik dre Mastodon.",
"compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.", "compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.",
@ -158,6 +167,7 @@
"confirmations.discard_edit_media.message": "Bez ez eus kemmoù n'int ket enrollet e deskrivadur ar media pe ar rakwel, nullañ anezho evelato?", "confirmations.discard_edit_media.message": "Bez ez eus kemmoù n'int ket enrollet e deskrivadur ar media pe ar rakwel, nullañ anezho evelato?",
"confirmations.domain_block.confirm": "Berzañ an domani a-bezh", "confirmations.domain_block.confirm": "Berzañ an domani a-bezh",
"confirmations.domain_block.message": "Ha sur oc'h e fell deoc'h berzañ an {domain} a-bezh? Peurvuiañ eo trawalc'h berzañ pe mudañ un nebeud implijer·ezed·ien. Ne welot danvez ebet o tont eus an domani-mañ. Dilamet e vo ar c'houmanantoù war an domani-mañ.", "confirmations.domain_block.message": "Ha sur oc'h e fell deoc'h berzañ an {domain} a-bezh? Peurvuiañ eo trawalc'h berzañ pe mudañ un nebeud implijer·ezed·ien. Ne welot danvez ebet o tont eus an domani-mañ. Dilamet e vo ar c'houmanantoù war an domani-mañ.",
"confirmations.edit.confirm": "Kemmañ",
"confirmations.logout.confirm": "Digevreañ", "confirmations.logout.confirm": "Digevreañ",
"confirmations.logout.message": "Ha sur oc'h e fell deoc'h digevreañ ?", "confirmations.logout.message": "Ha sur oc'h e fell deoc'h digevreañ ?",
"confirmations.mute.confirm": "Kuzhat", "confirmations.mute.confirm": "Kuzhat",
@ -172,7 +182,9 @@
"conversation.mark_as_read": "Merkañ evel lennet", "conversation.mark_as_read": "Merkañ evel lennet",
"conversation.open": "Gwelout ar gaozeadenn", "conversation.open": "Gwelout ar gaozeadenn",
"conversation.with": "Gant {names}", "conversation.with": "Gant {names}",
"copy_icon_button.copied": "Eilet er golver",
"copypaste.copied": "Eilet", "copypaste.copied": "Eilet",
"copypaste.copy_to_clipboard": "Eilañ er golver",
"directory.federated": "Eus ar fedibed anavezet", "directory.federated": "Eus ar fedibed anavezet",
"directory.local": "Eus {domain} hepken", "directory.local": "Eus {domain} hepken",
"directory.new_arrivals": "Degouezhet a-nevez", "directory.new_arrivals": "Degouezhet a-nevez",
@ -209,7 +221,8 @@
"empty_column.domain_blocks": "N'eus domani kuzh ebet c'hoazh.", "empty_column.domain_blocks": "N'eus domani kuzh ebet c'hoazh.",
"empty_column.explore_statuses": "N'eus tuadur ebet evit c'hoazh. Distroit diwezhatoc'h !", "empty_column.explore_statuses": "N'eus tuadur ebet evit c'hoazh. Distroit diwezhatoc'h !",
"empty_column.follow_requests": "N'ho peus reked heuliañ ebet c'hoazh. Pa vo resevet unan e teuio war wel amañ.", "empty_column.follow_requests": "N'ho peus reked heuliañ ebet c'hoazh. Pa vo resevet unan e teuio war wel amañ.",
"empty_column.hashtag": "N'eus netra er ger-klik-mañ c'hoazh.", "empty_column.followed_tags": "N'emaoc'h oc'h heuliañ hashtag ebet evit poent. Pa vioc'h e vo d'o gwelet amañ.",
"empty_column.hashtag": "N'eus netra en hashtag-mañ c'hoazh.",
"empty_column.home": "Goullo eo ho red-amzer degemer! Kit da weladenniñ {public} pe implijit ar c'hlask evit kregiñ ganti ha kejañ gant implijer·ien·ezed all.", "empty_column.home": "Goullo eo ho red-amzer degemer! Kit da weladenniñ {public} pe implijit ar c'hlask evit kregiñ ganti ha kejañ gant implijer·ien·ezed all.",
"empty_column.list": "Goullo eo al listenn-mañ evit c'hoazh. Pa vo embannet toudoù nevez gant e izili e teuint war wel amañ.", "empty_column.list": "Goullo eo al listenn-mañ evit c'hoazh. Pa vo embannet toudoù nevez gant e izili e teuint war wel amañ.",
"empty_column.lists": "N'ho peus roll ebet c'hoazh. Pa vo krouet unan ganeoc'h e vo diskouezet amañ.", "empty_column.lists": "N'ho peus roll ebet c'hoazh. Pa vo krouet unan ganeoc'h e vo diskouezet amañ.",
@ -223,7 +236,11 @@
"errors.unexpected_crash.copy_stacktrace": "Eilañ ar roudoù diveugañ er golver", "errors.unexpected_crash.copy_stacktrace": "Eilañ ar roudoù diveugañ er golver",
"errors.unexpected_crash.report_issue": "Danevellañ ur fazi", "errors.unexpected_crash.report_issue": "Danevellañ ur fazi",
"explore.search_results": "Disoc'hoù an enklask", "explore.search_results": "Disoc'hoù an enklask",
"explore.suggested_follows": "Tud",
"explore.title": "Furchal", "explore.title": "Furchal",
"explore.trending_links": "Keleier",
"explore.trending_statuses": "Embannadurioù",
"explore.trending_tags": "Hashtagoù",
"filter_modal.added.context_mismatch_title": "Kenarroud digenglotus !", "filter_modal.added.context_mismatch_title": "Kenarroud digenglotus !",
"filter_modal.added.expired_title": "Sil deuet d'e dermen !", "filter_modal.added.expired_title": "Sil deuet d'e dermen !",
"filter_modal.added.review_and_configure_title": "Arventennoù ar sil", "filter_modal.added.review_and_configure_title": "Arventennoù ar sil",
@ -237,9 +254,13 @@
"filter_modal.select_filter.subtitle": "Implijout ur rummad a zo anezhañ pe krouiñ unan nevez", "filter_modal.select_filter.subtitle": "Implijout ur rummad a zo anezhañ pe krouiñ unan nevez",
"filter_modal.select_filter.title": "Silañ an toud-mañ", "filter_modal.select_filter.title": "Silañ an toud-mañ",
"filter_modal.title.status": "Silañ un toud", "filter_modal.title.status": "Silañ un toud",
"firehose.all": "Pep tra",
"firehose.local": "Ar servijer-mañ",
"firehose.remote": "Servijerioù all",
"follow_request.authorize": "Aotren", "follow_request.authorize": "Aotren",
"follow_request.reject": "Nac'hañ", "follow_request.reject": "Nac'hañ",
"follow_requests.unlocked_explanation": "Daoust ma n'eo ket ho kont prennet, skipailh {domain} a soñj e fellfe deoc'h gwiriekaat pedadennoù heuliañ deus ar c'hontoù-se diwar-zorn.", "follow_requests.unlocked_explanation": "Daoust ma n'eo ket ho kont prennet, skipailh {domain} a soñj e fellfe deoc'h gwiriekaat pedadennoù heuliañ deus ar c'hontoù-se diwar-zorn.",
"followed_tags": "Hashtagoù o heuliañ",
"footer.about": "Diwar-benn", "footer.about": "Diwar-benn",
"footer.directory": "Kavlec'h ar profiloù", "footer.directory": "Kavlec'h ar profiloù",
"footer.get_app": "Pellgargañ an arload", "footer.get_app": "Pellgargañ an arload",
@ -247,29 +268,40 @@
"footer.keyboard_shortcuts": "Berradennoù klavier", "footer.keyboard_shortcuts": "Berradennoù klavier",
"footer.privacy_policy": "Reolennoù prevezded", "footer.privacy_policy": "Reolennoù prevezded",
"footer.source_code": "Gwelet kod mammenn", "footer.source_code": "Gwelet kod mammenn",
"footer.status": "Statud",
"generic.saved": "Enrollet", "generic.saved": "Enrollet",
"getting_started.heading": "Loc'hañ", "getting_started.heading": "Loc'hañ",
"hashtag.column_header.tag_mode.all": "ha {additional}", "hashtag.column_header.tag_mode.all": "ha(g) {additional}",
"hashtag.column_header.tag_mode.any": "pe {additional}", "hashtag.column_header.tag_mode.any": "pe {additional}",
"hashtag.column_header.tag_mode.none": "hep {additional}", "hashtag.column_header.tag_mode.none": "hep {additional}",
"hashtag.column_settings.select.no_options_message": "N'eus bet kavet ali ebet", "hashtag.column_settings.select.no_options_message": "N'eus bet kavet ali ebet",
"hashtag.column_settings.select.placeholder": "Ouzhpennañ geri-klik…", "hashtag.column_settings.select.placeholder": "Ouzhpennañ hashtagoù…",
"hashtag.column_settings.tag_mode.all": "An holl elfennoù-mañ", "hashtag.column_settings.tag_mode.all": "An holl anezho",
"hashtag.column_settings.tag_mode.any": "Unan e mesk anezho", "hashtag.column_settings.tag_mode.any": "Unan e mesk anezho",
"hashtag.column_settings.tag_mode.none": "Hini ebet anezho", "hashtag.column_settings.tag_mode.none": "Hini ebet anezho",
"hashtag.column_settings.tag_toggle": "Endelc'her gerioù-alc'hwez ouzhpenn evit ar bannad-mañ", "hashtag.column_settings.tag_toggle": "Endelc'her gerioù-alc'hwez ouzhpenn evit ar bannad-mañ",
"hashtag.counter_by_uses": "{count, plural, one {{counter} embannadur} other {{counter} embannadur}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} embannadur} other {{counter} embannadur}} hiziv",
"hashtag.follow": "Heuliañ ar ger-klik", "hashtag.follow": "Heuliañ ar ger-klik",
"hashtag.unfollow": "Diheuliañ ar ger-klik", "hashtag.unfollow": "Paouez heuliañ an hashtag",
"hashtags.and_other": "…{count, plural, one {hag # all} other {ha # all}}",
"home.actions.go_to_explore": "Gwelet petra zo diouzh ar c'hiz",
"home.actions.go_to_suggestions": "Kavout tud da heuliañ",
"home.column_settings.basic": "Diazez", "home.column_settings.basic": "Diazez",
"home.column_settings.show_reblogs": "Diskouez ar skignadennoù", "home.column_settings.show_reblogs": "Diskouez ar skignadennoù",
"home.column_settings.show_replies": "Diskouez ar respontoù", "home.column_settings.show_replies": "Diskouez ar respontoù",
"home.explore_prompt.title": "Homañ eo ho pajenn degemer e-barzh Mastodon.",
"home.hide_announcements": "Kuzhat ar c'hemennoù", "home.hide_announcements": "Kuzhat ar c'hemennoù",
"home.pending_critical_update.body": "Hizivait ho servijer Mastodon kerkent ha ma c'hallit mar plij!",
"home.pending_critical_update.link": "Gwelet an hizivadennoù",
"home.show_announcements": "Diskouez ar c'hemennoù", "home.show_announcements": "Diskouez ar c'hemennoù",
"interaction_modal.description.follow": "Gant ur gont Mastodon e c'hellit heuliañ {name} evit resev an toudoù a embann war ho red degemer.", "interaction_modal.description.follow": "Gant ur gont Mastodon e c'hellit heuliañ {name} evit resev an toudoù a embann war ho red degemer.",
"interaction_modal.description.reblog": "Gant ur gont Mastodon e c'hellit skignañ an toud-mañ evit rannañ anezhañ gant ho heulierien·ezed.", "interaction_modal.description.reblog": "Gant ur gont Mastodon e c'hellit skignañ an toud-mañ evit rannañ anezhañ gant ho heulierien·ezed.",
"interaction_modal.description.reply": "Gant ur gont Mastodon e c'hellit respont d'an toud-mañ.", "interaction_modal.description.reply": "Gant ur gont Mastodon e c'hellit respont d'an toud-mañ.",
"interaction_modal.no_account_yet": "N'eo ket war vMastodon?",
"interaction_modal.on_another_server": "War ur servijer all", "interaction_modal.on_another_server": "War ur servijer all",
"interaction_modal.on_this_server": "War ar servijer-mañ", "interaction_modal.on_this_server": "War ar servijer-mañ",
"interaction_modal.title.favourite": "Ouzhpennañ embannadur {name} d'ar re vuiañ-karet",
"interaction_modal.title.follow": "Heuliañ {name}", "interaction_modal.title.follow": "Heuliañ {name}",
"interaction_modal.title.reblog": "Skignañ toud {name}", "interaction_modal.title.reblog": "Skignañ toud {name}",
"interaction_modal.title.reply": "Respont da doud {name}", "interaction_modal.title.reply": "Respont da doud {name}",
@ -285,6 +317,8 @@
"keyboard_shortcuts.direct": "to open direct messages column", "keyboard_shortcuts.direct": "to open direct messages column",
"keyboard_shortcuts.down": "Diskennañ er roll", "keyboard_shortcuts.down": "Diskennañ er roll",
"keyboard_shortcuts.enter": "Digeriñ an toud", "keyboard_shortcuts.enter": "Digeriñ an toud",
"keyboard_shortcuts.favourite": "Ouzhpennañ an embannadur d'ar re vuiañ-karet",
"keyboard_shortcuts.favourites": "Digeriñ roll an embannadurioù muiañ-karet",
"keyboard_shortcuts.federated": "Digeriñ ar red-amzer kevredet", "keyboard_shortcuts.federated": "Digeriñ ar red-amzer kevredet",
"keyboard_shortcuts.heading": "Berradennoù klavier", "keyboard_shortcuts.heading": "Berradennoù klavier",
"keyboard_shortcuts.home": "Digeriñ ho red-amzer degemer", "keyboard_shortcuts.home": "Digeriñ ho red-amzer degemer",
@ -314,6 +348,7 @@
"lightbox.next": "Da-heul", "lightbox.next": "Da-heul",
"lightbox.previous": "A-raok", "lightbox.previous": "A-raok",
"limited_account_hint.action": "Diskouez an aelad memes tra", "limited_account_hint.action": "Diskouez an aelad memes tra",
"link_preview.author": "Gant {name}",
"lists.account.add": "Ouzhpennañ d'al listenn", "lists.account.add": "Ouzhpennañ d'al listenn",
"lists.account.remove": "Lemel kuit eus al listenn", "lists.account.remove": "Lemel kuit eus al listenn",
"lists.delete": "Dilemel al listenn", "lists.delete": "Dilemel al listenn",
@ -328,6 +363,7 @@
"lists.search": "Klask e-touez tud heuliet ganeoc'h", "lists.search": "Klask e-touez tud heuliet ganeoc'h",
"lists.subheading": "Ho listennoù", "lists.subheading": "Ho listennoù",
"load_pending": "{count, plural, one {# dra nevez} other {# dra nevez}}", "load_pending": "{count, plural, one {# dra nevez} other {# dra nevez}}",
"loading_indicator.label": "O kargañ…",
"media_gallery.toggle_visible": "{number, plural, one {Kuzhat ar skeudenn} other {Kuzhat ar skeudenn}}", "media_gallery.toggle_visible": "{number, plural, one {Kuzhat ar skeudenn} other {Kuzhat ar skeudenn}}",
"mute_modal.duration": "Padelezh", "mute_modal.duration": "Padelezh",
"mute_modal.hide_notifications": "Kuzhat kemenadennoù eus an implijer-se ?", "mute_modal.hide_notifications": "Kuzhat kemenadennoù eus an implijer-se ?",
@ -341,8 +377,10 @@
"navigation_bar.domain_blocks": "Domanioù kuzhet", "navigation_bar.domain_blocks": "Domanioù kuzhet",
"navigation_bar.edit_profile": "Kemmañ ar profil", "navigation_bar.edit_profile": "Kemmañ ar profil",
"navigation_bar.explore": "Furchal", "navigation_bar.explore": "Furchal",
"navigation_bar.favourites": "Muiañ-karet",
"navigation_bar.filters": "Gerioù kuzhet", "navigation_bar.filters": "Gerioù kuzhet",
"navigation_bar.follow_requests": "Pedadoù heuliañ", "navigation_bar.follow_requests": "Pedadoù heuliañ",
"navigation_bar.followed_tags": "Hashtagoù o heuliañ",
"navigation_bar.follows_and_followers": "Heuliadennoù ha heulier·ezed·ien", "navigation_bar.follows_and_followers": "Heuliadennoù ha heulier·ezed·ien",
"navigation_bar.lists": "Listennoù", "navigation_bar.lists": "Listennoù",
"navigation_bar.logout": "Digennaskañ", "navigation_bar.logout": "Digennaskañ",
@ -369,6 +407,7 @@
"notifications.column_settings.admin.report": "Disklêriadurioù nevez :", "notifications.column_settings.admin.report": "Disklêriadurioù nevez :",
"notifications.column_settings.admin.sign_up": "Enskrivadurioù nevez :", "notifications.column_settings.admin.sign_up": "Enskrivadurioù nevez :",
"notifications.column_settings.alert": "Kemennoù war ar burev", "notifications.column_settings.alert": "Kemennoù war ar burev",
"notifications.column_settings.favourite": "Muiañ-karet:",
"notifications.column_settings.filter_bar.advanced": "Skrammañ an-holl rummadoù", "notifications.column_settings.filter_bar.advanced": "Skrammañ an-holl rummadoù",
"notifications.column_settings.filter_bar.category": "Barrenn siloù prim", "notifications.column_settings.filter_bar.category": "Barrenn siloù prim",
"notifications.column_settings.filter_bar.show_bar": "Diskouezh barrenn siloù", "notifications.column_settings.filter_bar.show_bar": "Diskouezh barrenn siloù",
@ -386,6 +425,7 @@
"notifications.column_settings.update": "Kemmoù :", "notifications.column_settings.update": "Kemmoù :",
"notifications.filter.all": "Pep tra", "notifications.filter.all": "Pep tra",
"notifications.filter.boosts": "Skignadennoù", "notifications.filter.boosts": "Skignadennoù",
"notifications.filter.favourites": "Muiañ-karet",
"notifications.filter.follows": "Heuliañ", "notifications.filter.follows": "Heuliañ",
"notifications.filter.mentions": "Menegoù", "notifications.filter.mentions": "Menegoù",
"notifications.filter.polls": "Disoc'hoù ar sontadegoù", "notifications.filter.polls": "Disoc'hoù ar sontadegoù",
@ -399,15 +439,28 @@
"notifications_permission_banner.enable": "Lezel kemennoù war ar burev", "notifications_permission_banner.enable": "Lezel kemennoù war ar burev",
"notifications_permission_banner.how_to_control": "Evit reseviñ kemennoù pa ne vez ket digoret Mastodon, lezelit kemennoù war ar burev. Gallout a rit kontrollañ peseurt eskemmoù a c'henel kemennoù war ar burev gant ar {icon} nozelenn a-us kentre ma'z int lezelet.", "notifications_permission_banner.how_to_control": "Evit reseviñ kemennoù pa ne vez ket digoret Mastodon, lezelit kemennoù war ar burev. Gallout a rit kontrollañ peseurt eskemmoù a c'henel kemennoù war ar burev gant ar {icon} nozelenn a-us kentre ma'z int lezelet.",
"notifications_permission_banner.title": "Na vankit netra morse", "notifications_permission_banner.title": "Na vankit netra morse",
"onboarding.action.back": "Distreiñ",
"onboarding.actions.back": "Distreiñ",
"onboarding.actions.go_to_explore": "See what's trending", "onboarding.actions.go_to_explore": "See what's trending",
"onboarding.actions.go_to_home": "Go to your home feed", "onboarding.actions.go_to_home": "Go to your home feed",
"onboarding.compose.template": "Salud #Mastodon!",
"onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!", "onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
"onboarding.follows.title": "Popular on Mastodon", "onboarding.follows.title": "Popular on Mastodon",
"onboarding.profile.display_name": "Anv diskouezet",
"onboarding.profile.display_name_hint": "Hoc'h anv klok pe hoc'h anv fentus…",
"onboarding.profile.note": "Berr-ha-berr",
"onboarding.profile.note_hint": "Gallout a rit @menegiñ tud all pe #hashtagoù…",
"onboarding.profile.save_and_continue": "Enrollañ ha kenderc'hel",
"onboarding.profile.upload_avatar": "Enporzhiañ ur skeudenn profil",
"onboarding.share.message": "Me a zo {username} war #Mastodon! Heuilhit ac'hanon war {url}",
"onboarding.share.title": "Skignañ ho profil",
"onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:", "onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:",
"onboarding.start.skip": "Want to skip right ahead?", "onboarding.start.skip": "Want to skip right ahead?",
"onboarding.start.title": "Deuet oc'h a-benn!",
"onboarding.steps.follow_people.body": "You curate your own feed. Lets fill it with interesting people.", "onboarding.steps.follow_people.body": "You curate your own feed. Lets fill it with interesting people.",
"onboarding.steps.follow_people.title": "Follow {count, plural, one {one person} other {# people}}", "onboarding.steps.follow_people.title": "Follow {count, plural, one {one person} other {# people}}",
"onboarding.steps.publish_status.body": "Say hello to the world.", "onboarding.steps.publish_status.body": "Say hello to the world.",
"onboarding.steps.publish_status.title": "Grit hoc'h embannadur kentañ",
"onboarding.steps.setup_profile.body": "Others are more likely to interact with you with a filled out profile.", "onboarding.steps.setup_profile.body": "Others are more likely to interact with you with a filled out profile.",
"onboarding.steps.setup_profile.title": "Customize your profile", "onboarding.steps.setup_profile.title": "Customize your profile",
"onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon!", "onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon!",
@ -415,6 +468,7 @@
"picture_in_picture.restore": "Adlakaat", "picture_in_picture.restore": "Adlakaat",
"poll.closed": "Serret", "poll.closed": "Serret",
"poll.refresh": "Azbevaat", "poll.refresh": "Azbevaat",
"poll.reveal": "Gwelet an disoc'hoù",
"poll.total_people": "{count, plural, one {# den} other {# a zen}}", "poll.total_people": "{count, plural, one {# den} other {# a zen}}",
"poll.total_votes": "{count, plural, one {# votadenn} other {# votadenn}}", "poll.total_votes": "{count, plural, one {# votadenn} other {# votadenn}}",
"poll.vote": "Mouezhiañ", "poll.vote": "Mouezhiañ",
@ -433,6 +487,7 @@
"privacy.unlisted.short": "Anlistennet", "privacy.unlisted.short": "Anlistennet",
"privacy_policy.last_updated": "Hizivadenn ziwezhañ {date}", "privacy_policy.last_updated": "Hizivadenn ziwezhañ {date}",
"privacy_policy.title": "Reolennoù Prevezded", "privacy_policy.title": "Reolennoù Prevezded",
"recommended": "Erbedet",
"refresh": "Freskaat", "refresh": "Freskaat",
"regeneration_indicator.label": "O kargañ…", "regeneration_indicator.label": "O kargañ…",
"regeneration_indicator.sublabel": "War brientiñ emañ ho red degemer!", "regeneration_indicator.sublabel": "War brientiñ emañ ho red degemer!",
@ -450,6 +505,7 @@
"reply_indicator.cancel": "Nullañ", "reply_indicator.cancel": "Nullañ",
"report.block": "Stankañ", "report.block": "Stankañ",
"report.block_explanation": "Ne vo ket gwelet toudoù ar gont-se ken. Ne welo ket ho toudoù ha ne c'hello ket ho heuliañ ken. Gouzout a raio eo bet stanket ganeoc'h.", "report.block_explanation": "Ne vo ket gwelet toudoù ar gont-se ken. Ne welo ket ho toudoù ha ne c'hello ket ho heuliañ ken. Gouzout a raio eo bet stanket ganeoc'h.",
"report.categories.legal": "Lezennel",
"report.categories.other": "All", "report.categories.other": "All",
"report.categories.spam": "Spam", "report.categories.spam": "Spam",
"report.categories.violation": "Torret e vez gant an endalc'had unan pe meur a reolenn", "report.categories.violation": "Torret e vez gant an endalc'had unan pe meur a reolenn",
@ -467,6 +523,7 @@
"report.placeholder": "Askelennoù ouzhpenn", "report.placeholder": "Askelennoù ouzhpenn",
"report.reasons.dislike": "Ne blij ket din", "report.reasons.dislike": "Ne blij ket din",
"report.reasons.dislike_description": "An dra-se na fell ket deoc'h gwelet", "report.reasons.dislike_description": "An dra-se na fell ket deoc'h gwelet",
"report.reasons.legal": "Enep al lezenn eo",
"report.reasons.other": "Un abeg all eo", "report.reasons.other": "Un abeg all eo",
"report.reasons.other_description": "Ar gudenn na glot ket gant ar rummadoù all", "report.reasons.other_description": "Ar gudenn na glot ket gant ar rummadoù all",
"report.reasons.spam": "Spam eo", "report.reasons.spam": "Spam eo",
@ -482,16 +539,31 @@
"report.thanks.title": "Ne fell ket deoc'h gwelet an dra-se ?", "report.thanks.title": "Ne fell ket deoc'h gwelet an dra-se ?",
"report.thanks.title_actionable": "Trugarez evit bezañ disklêriet, emaomp o vont da glask pelloc'h.", "report.thanks.title_actionable": "Trugarez evit bezañ disklêriet, emaomp o vont da glask pelloc'h.",
"report.unfollow": "Diheuliañ @{name}", "report.unfollow": "Diheuliañ @{name}",
"report_notification.attached_statuses": "{count, plural, one {# post} other {# posts}} attached", "report_notification.attached_statuses": "{count, plural, one {{count} embannadur} other {{count} embannadur}} stag",
"report_notification.categories.legal": "Lezennel",
"report_notification.categories.other": "All", "report_notification.categories.other": "All",
"report_notification.categories.spam": "Spam", "report_notification.categories.spam": "Spam",
"report_notification.categories.violation": "Torradur da reolennoù ar servijer", "report_notification.categories.violation": "Torradur da reolennoù ar servijer",
"report_notification.open": "Digeriñ an disklêriadur", "report_notification.open": "Digeriñ an disklêriadur",
"search.placeholder": "Klask", "search.placeholder": "Klask",
"search.quick_action.account_search": "Profiloù a glot gant {x}",
"search.quick_action.go_to_account": "Mont d'ar profil {x}",
"search.quick_action.go_to_hashtag": "Mont d'an hashtag {x}",
"search.quick_action.open_url": "Digeriñ an URL e-barzh Mastodon",
"search.quick_action.status_search": "Embannadurioù a glot gant {x}",
"search.search_or_paste": "Klask pe pegañ un URL", "search.search_or_paste": "Klask pe pegañ un URL",
"search_popout.full_text_search_disabled_message": "N'eo ket da gaout war {domain}.",
"search_popout.language_code": "Kod yezh ISO",
"search_popout.options": "Dibarzhioù klask",
"search_popout.quick_actions": "Oberoù prim",
"search_popout.recent": "Klaskoù nevesañ",
"search_popout.specific_date": "deiziad resis",
"search_popout.user": "implijer·ez",
"search_results.accounts": "Profiloù",
"search_results.all": "Pep tra", "search_results.all": "Pep tra",
"search_results.hashtags": "Gerioù-klik", "search_results.hashtags": "Hashtagoù",
"search_results.nothing_found": "Disoc'h ebet gant ar gerioù-se", "search_results.nothing_found": "Disoc'h ebet gant ar gerioù-se",
"search_results.see_all": "Gwelet pep tra",
"search_results.statuses": "Toudoù", "search_results.statuses": "Toudoù",
"search_results.title": "Klask {q}", "search_results.title": "Klask {q}",
"server_banner.active_users": "implijerien·ezed oberiant", "server_banner.active_users": "implijerien·ezed oberiant",
@ -513,11 +585,15 @@
"status.edited": "Aozet {date}", "status.edited": "Aozet {date}",
"status.edited_x_times": "Edited {count, plural, one {# time} other {# times}}", "status.edited_x_times": "Edited {count, plural, one {# time} other {# times}}",
"status.embed": "Enframmañ", "status.embed": "Enframmañ",
"status.favourite": "Muiañ-karet",
"status.filter": "Silañ ar c'hannad-mañ", "status.filter": "Silañ ar c'hannad-mañ",
"status.filtered": "Silet", "status.filtered": "Silet",
"status.hide": "Kuzhat an embannadur",
"status.history.created": "Krouet gant {name} {date}", "status.history.created": "Krouet gant {name} {date}",
"status.history.edited": "Kemmet gant {name} {date}", "status.history.edited": "Kemmet gant {name} {date}",
"status.load_more": "Kargañ muioc'h", "status.load_more": "Kargañ muioc'h",
"status.media.open": "Klikit evit digeriñ",
"status.media.show": "Klikit evit diskouez",
"status.media_hidden": "Media kuzhet", "status.media_hidden": "Media kuzhet",
"status.mention": "Menegiñ @{name}", "status.mention": "Menegiñ @{name}",
"status.more": "Muioc'h", "status.more": "Muioc'h",
@ -592,6 +668,7 @@
"upload_modal.preview_label": "Rakwel ({ratio})", "upload_modal.preview_label": "Rakwel ({ratio})",
"upload_progress.label": "O pellgargañ...", "upload_progress.label": "O pellgargañ...",
"upload_progress.processing": "War ober…", "upload_progress.processing": "War ober…",
"username.taken": "Tapet eo an anv implijer-mañ dija. Klaskit skrivañ unan all",
"video.close": "Serriñ ar video", "video.close": "Serriñ ar video",
"video.download": "Pellgargañ ar restr", "video.download": "Pellgargañ ar restr",
"video.exit_fullscreen": "Kuitaat ar mod skramm leun", "video.exit_fullscreen": "Kuitaat ar mod skramm leun",

View File

@ -62,7 +62,7 @@
"account.requested": "Die Genehmigung steht noch aus. Klicke hier, um die Follower-Anfrage zurückzuziehen", "account.requested": "Die Genehmigung steht noch aus. Klicke hier, um die Follower-Anfrage zurückzuziehen",
"account.requested_follow": "{name} möchte dir folgen", "account.requested_follow": "{name} möchte dir folgen",
"account.share": "Profil von @{name} teilen", "account.share": "Profil von @{name} teilen",
"account.show_reblogs": "Geteilte Beiträge von @{name} wieder anzeigen", "account.show_reblogs": "Geteilte Beiträge von @{name} anzeigen",
"account.statuses_counter": "{count, plural, one {{counter} Beitrag} other {{counter} Beiträge}}", "account.statuses_counter": "{count, plural, one {{counter} Beitrag} other {{counter} Beiträge}}",
"account.unblock": "Blockierung von @{name} aufheben", "account.unblock": "Blockierung von @{name} aufheben",
"account.unblock_domain": "Blockierung von {domain} aufheben", "account.unblock_domain": "Blockierung von {domain} aufheben",

View File

@ -260,6 +260,9 @@
"filter_modal.select_filter.subtitle": "Χρησιμοποιήστε μια υπάρχουσα κατηγορία ή δημιουργήστε μια νέα", "filter_modal.select_filter.subtitle": "Χρησιμοποιήστε μια υπάρχουσα κατηγορία ή δημιουργήστε μια νέα",
"filter_modal.select_filter.title": "Φιλτράρισμα αυτής της ανάρτησης", "filter_modal.select_filter.title": "Φιλτράρισμα αυτής της ανάρτησης",
"filter_modal.title.status": "Φιλτράρισμα μιας ανάρτησης", "filter_modal.title.status": "Φιλτράρισμα μιας ανάρτησης",
"firehose.all": "Όλα",
"firehose.local": "Αυτός ο διακομιστής",
"firehose.remote": "Άλλοι διακομιστές",
"follow_request.authorize": "Εξουσιοδότησε", "follow_request.authorize": "Εξουσιοδότησε",
"follow_request.reject": "Απέρριψε", "follow_request.reject": "Απέρριψε",
"follow_requests.unlocked_explanation": "Παρόλο που ο λογαριασμός σου δεν είναι κλειδωμένος, το προσωπικό του {domain} θεώρησαν πως ίσως να θέλεις να ελέγξεις χειροκίνητα αυτά τα αιτήματα ακολούθησης.", "follow_requests.unlocked_explanation": "Παρόλο που ο λογαριασμός σου δεν είναι κλειδωμένος, το προσωπικό του {domain} θεώρησαν πως ίσως να θέλεις να ελέγξεις χειροκίνητα αυτά τα αιτήματα ακολούθησης.",
@ -285,11 +288,15 @@
"hashtag.column_settings.tag_toggle": "Προσθήκη επιπλέον ταμπελών για την κολώνα", "hashtag.column_settings.tag_toggle": "Προσθήκη επιπλέον ταμπελών για την κολώνα",
"hashtag.follow": "Παρακολούθηση ετικέτας", "hashtag.follow": "Παρακολούθηση ετικέτας",
"hashtag.unfollow": "Διακοπή παρακολούθησης ετικέτας", "hashtag.unfollow": "Διακοπή παρακολούθησης ετικέτας",
"home.actions.go_to_suggestions": "Βρείτε άτομα για να ακολουθήσετε",
"home.column_settings.basic": "Βασικές ρυθμίσεις", "home.column_settings.basic": "Βασικές ρυθμίσεις",
"home.column_settings.show_reblogs": "Εμφάνιση προωθήσεων", "home.column_settings.show_reblogs": "Εμφάνιση προωθήσεων",
"home.column_settings.show_replies": "Εμφάνιση απαντήσεων", "home.column_settings.show_replies": "Εμφάνιση απαντήσεων",
"home.explore_prompt.body": "Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. If that feels too quiet, you may want to:\nΗ τροφοδοσία της αρχικής σελίδας σας είναι ένα μίγμα από αναρτήσεις με τις ετικέτες και τα άτομα που επιλέξατε να ακολουθείτε, και τις αναρτήσεις που προωθούν. Εάν αυτό σας φαίνεται πολύ ήσυχο, μπορεί να θέλετε:", "home.explore_prompt.body": "Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. If that feels too quiet, you may want to:\nΗ τροφοδοσία της αρχικής σελίδας σας είναι ένα μίγμα από αναρτήσεις με τις ετικέτες και τα άτομα που επιλέξατε να ακολουθείτε, και τις αναρτήσεις που προωθούν. Εάν αυτό σας φαίνεται πολύ ήσυχο, μπορεί να θέλετε:",
"home.explore_prompt.title": "Αυτό είναι το σπίτι σας στο Mastodon.",
"home.hide_announcements": "Απόκρυψη ανακοινώσεων", "home.hide_announcements": "Απόκρυψη ανακοινώσεων",
"home.pending_critical_update.link": "Δείτε ενημερώσεις",
"home.pending_critical_update.title": "Κρίσιμη ενημέρωση ασφαλείας διαθέσιμη!",
"home.show_announcements": "Εμφάνιση ανακοινώσεων", "home.show_announcements": "Εμφάνιση ανακοινώσεων",
"interaction_modal.description.follow": "Με έναν λογαριασμό Mastodon, μπορείς να ακολουθήσεις τον/την {name} ώστε να λαμβάνεις τις αναρτήσεις του/της στη δική σου ροή.", "interaction_modal.description.follow": "Με έναν λογαριασμό Mastodon, μπορείς να ακολουθήσεις τον/την {name} ώστε να λαμβάνεις τις αναρτήσεις του/της στη δική σου ροή.",
"interaction_modal.description.reblog": "Με ένα λογαριασμό Mastodon, μπορείς να ενισχύσεις αυτή την ανάρτηση για να τη μοιραστείς με τους δικούς σου ακολούθους.", "interaction_modal.description.reblog": "Με ένα λογαριασμό Mastodon, μπορείς να ενισχύσεις αυτή την ανάρτηση για να τη μοιραστείς με τους δικούς σου ακολούθους.",
@ -314,6 +321,7 @@
"keyboard_shortcuts.direct": "για το άνοιγμα της στήλης ιδιωτικών επισημάνσεων", "keyboard_shortcuts.direct": "για το άνοιγμα της στήλης ιδιωτικών επισημάνσεων",
"keyboard_shortcuts.down": "κίνηση προς τα κάτω στη λίστα", "keyboard_shortcuts.down": "κίνηση προς τα κάτω στη λίστα",
"keyboard_shortcuts.enter": "Εμφάνιση ανάρτησης", "keyboard_shortcuts.enter": "Εμφάνιση ανάρτησης",
"keyboard_shortcuts.favourite": "Αγαπημένη δημοσίευση",
"keyboard_shortcuts.federated": "Άνοιγμα ροής συναλλαγών", "keyboard_shortcuts.federated": "Άνοιγμα ροής συναλλαγών",
"keyboard_shortcuts.heading": "Συντομεύσεις πληκτρολογίου", "keyboard_shortcuts.heading": "Συντομεύσεις πληκτρολογίου",
"keyboard_shortcuts.home": "Άνοιγμα ροής αρχικής σελίδας", "keyboard_shortcuts.home": "Άνοιγμα ροής αρχικής σελίδας",
@ -358,6 +366,7 @@
"lists.search": "Αναζήτησε μεταξύ των ανθρώπων που ακουλουθείς", "lists.search": "Αναζήτησε μεταξύ των ανθρώπων που ακουλουθείς",
"lists.subheading": "Οι λίστες σου", "lists.subheading": "Οι λίστες σου",
"load_pending": "{count, plural, one {# νέο στοιχείο} other {# νέα στοιχεία}}", "load_pending": "{count, plural, one {# νέο στοιχείο} other {# νέα στοιχεία}}",
"loading_indicator.label": "Φόρτωση…",
"media_gallery.toggle_visible": "{number, plural, one {Απόκρυψη εικόνας} other {Απόκρυψη εικόνων}}", "media_gallery.toggle_visible": "{number, plural, one {Απόκρυψη εικόνας} other {Απόκρυψη εικόνων}}",
"moved_to_account_banner.text": "Ο λογαριασμός σου {disabledAccount} είναι προσωρινά απενεργοποιημένος επειδή μεταφέρθηκες στον {movedToAccount}.", "moved_to_account_banner.text": "Ο λογαριασμός σου {disabledAccount} είναι προσωρινά απενεργοποιημένος επειδή μεταφέρθηκες στον {movedToAccount}.",
"mute_modal.duration": "Διάρκεια", "mute_modal.duration": "Διάρκεια",
@ -380,6 +389,7 @@
"navigation_bar.lists": "Λίστες", "navigation_bar.lists": "Λίστες",
"navigation_bar.logout": "Αποσύνδεση", "navigation_bar.logout": "Αποσύνδεση",
"navigation_bar.mutes": "Αποσιωπημένοι χρήστες", "navigation_bar.mutes": "Αποσιωπημένοι χρήστες",
"navigation_bar.opened_in_classic_interface": "Δημοσιεύσεις, λογαριασμοί και άλλες συγκεκριμένες σελίδες ανοίγονται από προεπιλογή στην κλασική διεπαφή ιστού.",
"navigation_bar.personal": "Προσωπικά", "navigation_bar.personal": "Προσωπικά",
"navigation_bar.pins": "Καρφιτσωμένες αναρτήσεις", "navigation_bar.pins": "Καρφιτσωμένες αναρτήσεις",
"navigation_bar.preferences": "Προτιμήσεις", "navigation_bar.preferences": "Προτιμήσεις",
@ -403,6 +413,7 @@
"notifications.column_settings.admin.report": "Νέες αναφορές:", "notifications.column_settings.admin.report": "Νέες αναφορές:",
"notifications.column_settings.admin.sign_up": "Νέες εγγραφές:", "notifications.column_settings.admin.sign_up": "Νέες εγγραφές:",
"notifications.column_settings.alert": "Ειδοποιήσεις επιφάνειας εργασίας", "notifications.column_settings.alert": "Ειδοποιήσεις επιφάνειας εργασίας",
"notifications.column_settings.favourite": "Αγαπημένα:",
"notifications.column_settings.filter_bar.advanced": "Εμφάνιση όλων των κατηγοριών", "notifications.column_settings.filter_bar.advanced": "Εμφάνιση όλων των κατηγοριών",
"notifications.column_settings.filter_bar.category": "Μπάρα γρήγορου φίλτρου", "notifications.column_settings.filter_bar.category": "Μπάρα γρήγορου φίλτρου",
"notifications.column_settings.filter_bar.show_bar": "Εμφάνιση μπάρας φίλτρου", "notifications.column_settings.filter_bar.show_bar": "Εμφάνιση μπάρας φίλτρου",

View File

@ -21,7 +21,7 @@
"account.blocked": "Estetty", "account.blocked": "Estetty",
"account.browse_more_on_origin_server": "Selaile lisää alkuperäisellä palvelimella", "account.browse_more_on_origin_server": "Selaile lisää alkuperäisellä palvelimella",
"account.cancel_follow_request": "Peruuta seurantapyyntö", "account.cancel_follow_request": "Peruuta seurantapyyntö",
"account.copy": "Kopioi profiililinkki", "account.copy": "Kopioi linkki profiiliin",
"account.direct": "Mainitse @{name} yksityisesti", "account.direct": "Mainitse @{name} yksityisesti",
"account.disable_notifications": "Lopeta ilmoittamasta minulle, kun @{name} julkaisee", "account.disable_notifications": "Lopeta ilmoittamasta minulle, kun @{name} julkaisee",
"account.domain_blocked": "Verkkotunnus estetty", "account.domain_blocked": "Verkkotunnus estetty",
@ -53,7 +53,7 @@
"account.mute_notifications_short": "Mykistä ilmoitukset", "account.mute_notifications_short": "Mykistä ilmoitukset",
"account.mute_short": "Mykistä", "account.mute_short": "Mykistä",
"account.muted": "Mykistetty", "account.muted": "Mykistetty",
"account.mutual": "Molemmat", "account.mutual": "Seuraatte toisianne",
"account.no_bio": "Kuvausta ei ole annettu.", "account.no_bio": "Kuvausta ei ole annettu.",
"account.open_original_page": "Avaa alkuperäinen sivu", "account.open_original_page": "Avaa alkuperäinen sivu",
"account.posts": "Julkaisut", "account.posts": "Julkaisut",
@ -193,7 +193,7 @@
"conversation.mark_as_read": "Merkitse luetuksi", "conversation.mark_as_read": "Merkitse luetuksi",
"conversation.open": "Näytä keskustelu", "conversation.open": "Näytä keskustelu",
"conversation.with": "{names} kanssa", "conversation.with": "{names} kanssa",
"copy_icon_button.copied": "Kopioitiin leikepöydälle", "copy_icon_button.copied": "Sisältö kopioitiin leikepöydälle",
"copypaste.copied": "Kopioitu", "copypaste.copied": "Kopioitu",
"copypaste.copy_to_clipboard": "Kopioi leikepöydälle", "copypaste.copy_to_clipboard": "Kopioi leikepöydälle",
"directory.federated": "Koko tunnettu fediversumi", "directory.federated": "Koko tunnettu fediversumi",
@ -483,10 +483,10 @@
"onboarding.follows.lead": "Kokoat oman kotisyötteesi itse. Mitä enemmän ihmisiä seuraat, sitä aktiivisempi ja kiinnostavampi syöte on. Nämä profiilit voivat olla alkuun hyvä lähtökohta — voit aina lopettaa niiden seuraamisen myöhemmin!", "onboarding.follows.lead": "Kokoat oman kotisyötteesi itse. Mitä enemmän ihmisiä seuraat, sitä aktiivisempi ja kiinnostavampi syöte on. Nämä profiilit voivat olla alkuun hyvä lähtökohta — voit aina lopettaa niiden seuraamisen myöhemmin!",
"onboarding.follows.title": "Mukauta kotisyötettäsi", "onboarding.follows.title": "Mukauta kotisyötettäsi",
"onboarding.profile.discoverable": "Aseta profiilini löydettäväksi", "onboarding.profile.discoverable": "Aseta profiilini löydettäväksi",
"onboarding.profile.discoverable_hint": "Kun olet määrittänyt itsesi löydettäväksi Mastodonista, julkaisusi voivat näkyä hakutuloksissa ja suosituissa kohteissa ja profiiliasi voidaan ehdottaa käyttäjille, jotka ovat kiinnostuneet samoista aiheista kuin sinä.", "onboarding.profile.discoverable_hint": "Kun olet määrittänyt itsesi löydettäväksi Mastodonista, julkaisusi voivat näkyä hakutuloksissa ja suosituissa kohteissa. Lisäksi profiiliasi voidaan ehdottaa käyttäjille, jotka ovat kiinnostuneita kanssasi samoista aiheista.",
"onboarding.profile.display_name": "Näyttönimi", "onboarding.profile.display_name": "Näyttönimi",
"onboarding.profile.display_name_hint": "Koko nimesi tai lempinimesi…", "onboarding.profile.display_name_hint": "Koko nimesi tai lempinimesi…",
"onboarding.profile.lead": "Voit viimeistellä tämän milloin tahansa asetuksista, jotka tarjoavat vielä enemmän mukautusvalintoja.", "onboarding.profile.lead": "Voit viimeistellä tämän milloin tahansa asetuksista. Sieltä löydät myös lisää mukautusvaihtoehtoja.",
"onboarding.profile.note": "Elämäkerta", "onboarding.profile.note": "Elämäkerta",
"onboarding.profile.note_hint": "Voit @mainita muita käyttäjiä tai #aihetunnisteita…", "onboarding.profile.note_hint": "Voit @mainita muita käyttäjiä tai #aihetunnisteita…",
"onboarding.profile.save_and_continue": "Tallenna ja jatka", "onboarding.profile.save_and_continue": "Tallenna ja jatka",

View File

@ -1,4 +1,9 @@
{ {
"about.blocks": "Servitores moderate",
"about.contact": "Contacto:",
"about.disclaimer": "Mastodon es software libere, de codice aperte, e un marca de Mastodon gGmbH.",
"about.rules": "Regulas del servitor",
"account.account_note_header": "Nota",
"account.add_or_remove_from_list": "Adder o remover ab listas", "account.add_or_remove_from_list": "Adder o remover ab listas",
"account.badges.group": "Gruppo", "account.badges.group": "Gruppo",
"account.block": "Blocar @{name}", "account.block": "Blocar @{name}",
@ -6,14 +11,43 @@
"account.blocked": "Blocate", "account.blocked": "Blocate",
"account.copy": "Copiar ligamine a profilo", "account.copy": "Copiar ligamine a profilo",
"account.edit_profile": "Modificar profilo", "account.edit_profile": "Modificar profilo",
"account.endorse": "Evidentiar sur le profilo",
"account.featured_tags.last_status_at": "Ultime message in {date}",
"account.featured_tags.last_status_never": "Necun messages",
"account.follow": "Sequer",
"account.follow_back": "Sequer etiam",
"account.followers": "Sequitores",
"account.following": "Sequente",
"account.go_to_profile": "Vader al profilo", "account.go_to_profile": "Vader al profilo",
"account.hide_reblogs": "Celar boosts de @{name}",
"account.languages": "Cambiar le linguas subscribite",
"account.link_verified_on": "Le proprietate de iste ligamine esseva verificate le {date}",
"account.locked_info": "Le stato de confidentialitate de iste conto es definite a blocate. Le proprietario revisa manualmente qui pote sequer lo.",
"account.mention": "Mentionar @{name}",
"account.moved_to": "{name} indicava que lor nove conto ora es:", "account.moved_to": "{name} indicava que lor nove conto ora es:",
"account.mute": "Silentiar @{name}",
"account.mute_notifications_short": "Silentiar le notificationes",
"account.mute_short": "Silentiar",
"account.muted": "Silentiate",
"account.no_bio": "Nulle description fornite.",
"account.posts": "Messages",
"account.posts_with_replies": "Messages e responsas",
"account.share": "Compartir profilo de @{name}", "account.share": "Compartir profilo de @{name}",
"account.show_reblogs": "Monstrar responsas de @{name}",
"account.unblock": "Disblocar @{name}", "account.unblock": "Disblocar @{name}",
"account.unblock_domain": "Disblocar dominio {domain}",
"account.unblock_short": "Disblocar", "account.unblock_short": "Disblocar",
"account.unendorse": "Non evidentiar sur le profilo", "account.unendorse": "Non evidentiar sur le profilo",
"account.unmute": "Non plus silentiar @{name}",
"account.unmute_notifications_short": "Non plus silentiar le notificationes",
"account.unmute_short": "Non plus silentiar",
"account_note.placeholder": "Clicca pro adder un nota", "account_note.placeholder": "Clicca pro adder un nota",
"admin.dashboard.retention.cohort_size": "Nove usatores", "admin.dashboard.retention.cohort_size": "Nove usatores",
"admin.impact_report.instance_followers": "Sequitores que nostre usatores poterea perder",
"admin.impact_report.instance_follows": "Sequitores que lor usatores poterea perder",
"alert.rate_limited.message": "Retenta depost {retry_time, time, medium}.",
"alert.unexpected.message": "Ocurreva un error inexpectate.",
"announcement.announcement": "Annuncio",
"audio.hide": "Celar audio", "audio.hide": "Celar audio",
"autosuggest_hashtag.per_week": "{count} per septimana", "autosuggest_hashtag.per_week": "{count} per septimana",
"bundle_column_error.network.title": "Error de rete", "bundle_column_error.network.title": "Error de rete",
@ -21,64 +55,260 @@
"bundle_column_error.return": "Retornar al initio", "bundle_column_error.return": "Retornar al initio",
"bundle_modal_error.close": "Clauder", "bundle_modal_error.close": "Clauder",
"bundle_modal_error.retry": "Tentar novemente", "bundle_modal_error.retry": "Tentar novemente",
"closed_registrations_modal.find_another_server": "Trovar altere servitor",
"column.about": "A proposito de",
"column.blocks": "Usatores blocate", "column.blocks": "Usatores blocate",
"column.bookmarks": "Marcapaginas",
"column.community": "Chronologia local",
"column.direct": "Mentiones private",
"column.directory": "Navigar profilos", "column.directory": "Navigar profilos",
"column.domain_blocks": "Dominios blocate",
"column.favourites": "Favoritos", "column.favourites": "Favoritos",
"column.firehose": "Fluxos in directe",
"column.home": "Initio", "column.home": "Initio",
"column.lists": "Listas", "column.lists": "Listas",
"column.mutes": "Usatores silentiate",
"column.notifications": "Notificationes", "column.notifications": "Notificationes",
"column.public": "Chronologia federate",
"column_header.hide_settings": "Celar le parametros", "column_header.hide_settings": "Celar le parametros",
"column_header.moveLeft_settings": "Mover columna al sinistra",
"column_header.moveRight_settings": "Mover columna al dextra",
"column_header.show_settings": "Monstrar le parametros", "column_header.show_settings": "Monstrar le parametros",
"column_subheading.settings": "Parametros", "column_subheading.settings": "Parametros",
"compose.language.change": "Cambiar le lingua", "compose.language.change": "Cambiar le lingua",
"compose.language.search": "Cercar linguas...", "compose.language.search": "Cercar linguas...",
"compose.published.body": "Message publicate.",
"compose.published.open": "Aperir", "compose.published.open": "Aperir",
"compose.saved.body": "Message salvate.",
"compose_form.direct_message_warning_learn_more": "Apprender plus",
"compose_form.lock_disclaimer": "Tu conto non es {locked}. Quicunque pote sequer te pro vider tu messages solo pro sequitores.",
"compose_form.lock_disclaimer.lock": "blocate",
"compose_form.poll.add_option": "Adder un option", "compose_form.poll.add_option": "Adder un option",
"compose_form.poll.remove_option": "Remover iste option", "compose_form.poll.remove_option": "Remover iste option",
"compose_form.publish": "Publicar",
"compose_form.publish_form": "Nove message",
"compose_form.publish_loud": "{publish}!",
"compose_form.save_changes": "Salvar le cambiamentos",
"compose_form.spoiler.marked": "Remover advertimento de contento",
"compose_form.spoiler.unmarked": "Adder advertimento de contento",
"compose_form.spoiler_placeholder": "Scribe tu advertimento hic",
"confirmation_modal.cancel": "Cancellar", "confirmation_modal.cancel": "Cancellar",
"confirmations.block.confirm": "Blocar",
"confirmations.delete.confirm": "Deler", "confirmations.delete.confirm": "Deler",
"confirmations.delete.message": "Es tu secur que tu vole deler iste message?",
"confirmations.delete_list.confirm": "Deler", "confirmations.delete_list.confirm": "Deler",
"confirmations.delete_list.message": "Es tu secur que tu vole deler permanentemente iste lista?",
"confirmations.domain_block.confirm": "Blocar le dominio complete",
"confirmations.edit.confirm": "Modificar",
"confirmations.logout.confirm": "Clauder le session", "confirmations.logout.confirm": "Clauder le session",
"confirmations.logout.message": "Es tu secur que tu vole clauder le session?",
"confirmations.mute.confirm": "Silentiar",
"confirmations.mute.message": "Es tu secur que tu vole silentiar {name}?",
"confirmations.reply.confirm": "Responder",
"conversation.delete": "Deler conversation",
"conversation.mark_as_read": "Marcar como legite",
"conversation.open": "Vider conversation",
"conversation.with": "Con {names}",
"copy_icon_button.copied": "Copiate al area de transferentia", "copy_icon_button.copied": "Copiate al area de transferentia",
"copypaste.copied": "Copiate",
"copypaste.copy_to_clipboard": "Copiar al area de transferentia", "copypaste.copy_to_clipboard": "Copiar al area de transferentia",
"directory.federated": "Ab le fediverso cognoscite",
"directory.local": "Solmente ab {domain}",
"directory.recently_active": "Recentemente active",
"disabled_account_banner.account_settings": "Parametros de conto", "disabled_account_banner.account_settings": "Parametros de conto",
"disabled_account_banner.text": "Tu conto {disabledAccount} es actualmente disactivate.",
"dismissable_banner.dismiss": "Dimitter", "dismissable_banner.dismiss": "Dimitter",
"emoji_button.activity": "Activitate", "emoji_button.activity": "Activitate",
"emoji_button.clear": "Rader",
"emoji_button.custom": "Personalisate", "emoji_button.custom": "Personalisate",
"emoji_button.recent": "Frequentemente usate",
"emoji_button.search": "Cercar...",
"emoji_button.search_results": "Resultatos de recerca", "emoji_button.search_results": "Resultatos de recerca",
"empty_column.account_suspended": "Conto suspendite",
"empty_column.account_timeline": "Nulle messages hic!",
"empty_column.account_unavailable": "Profilo non disponibile", "empty_column.account_unavailable": "Profilo non disponibile",
"empty_column.blocks": "Tu non ha blocate alcun usator ancora.",
"errors.unexpected_crash.report_issue": "Signalar un defecto", "errors.unexpected_crash.report_issue": "Signalar un defecto",
"explore.search_results": "Resultatos de recerca", "explore.search_results": "Resultatos de recerca",
"explore.title": "Explorar",
"explore.trending_links": "Novas", "explore.trending_links": "Novas",
"explore.trending_statuses": "Messages",
"explore.trending_tags": "Hashtags",
"filter_modal.added.review_and_configure_title": "Parametros de filtro",
"filter_modal.added.settings_link": "pagina de parametros",
"filter_modal.added.short_explanation": "Iste message esseva addite al sequente categoria de filtros: {title}.",
"filter_modal.added.title": "Filtro addite!",
"filter_modal.select_filter.prompt_new": "Nove categoria: {name}",
"filter_modal.select_filter.search": "Cercar o crear",
"filter_modal.select_filter.title": "Filtrar iste message",
"filter_modal.title.status": "Filtrar un message",
"firehose.all": "Toto", "firehose.all": "Toto",
"firehose.local": "Iste servitor", "firehose.local": "Iste servitor",
"firehose.remote": "Altere servitores", "firehose.remote": "Altere servitores",
"footer.about": "A proposito de", "footer.about": "A proposito de",
"footer.directory": "Directorio de profilos", "footer.directory": "Directorio de profilos",
"footer.get_app": "Obtene le application",
"footer.keyboard_shortcuts": "Accessos directe de claviero",
"footer.privacy_policy": "Politica de confidentialitate", "footer.privacy_policy": "Politica de confidentialitate",
"footer.source_code": "Vider le codice fonte", "footer.source_code": "Vider le codice fonte",
"footer.status": "Stato", "footer.status": "Stato",
"generic.saved": "Salvate",
"getting_started.heading": "Prime passos",
"hashtag.column_header.tag_mode.all": "e {additional}",
"hashtag.column_header.tag_mode.any": "o {additional}",
"hashtag.column_settings.select.no_options_message": "Nulle suggestiones trovate",
"hashtag.column_settings.select.placeholder": "Insere hashtags…",
"hashtag.follow": "Sequer hashtag",
"home.column_settings.show_reblogs": "Monstrar boosts",
"home.column_settings.show_replies": "Monstrar responsas",
"home.hide_announcements": "Celar annuncios",
"home.pending_critical_update.body": "Actualisa tu servitor de Mastodon le plus tosto possibile!",
"home.pending_critical_update.link": "Vider actualisationes", "home.pending_critical_update.link": "Vider actualisationes",
"home.show_announcements": "Monstrar annuncios",
"interaction_modal.no_account_yet": "Non sur Mstodon?",
"interaction_modal.on_another_server": "In un servitor differente",
"interaction_modal.on_this_server": "In iste servitor",
"interaction_modal.title.follow": "Sequer {name}",
"interaction_modal.title.reblog": "Facer boost al message de {name}",
"interaction_modal.title.reply": "Responder al message de {name}",
"keyboard_shortcuts.blocked": "Aperir lista de usatores blocate",
"keyboard_shortcuts.boost": "Facer boost al message",
"keyboard_shortcuts.description": "Description",
"keyboard_shortcuts.enter": "Aperir message",
"keyboard_shortcuts.favourites": "Aperir lista de favoritos",
"keyboard_shortcuts.federated": "Aperir le chronologia federate",
"keyboard_shortcuts.heading": "Accessos directe de claviero",
"keyboard_shortcuts.home": "Aperir le chronologia de initio",
"keyboard_shortcuts.local": "Aperir le chronologia local",
"keyboard_shortcuts.muted": "Aperir lista de usatores silentiate",
"keyboard_shortcuts.my_profile": "Aperir tu profilo", "keyboard_shortcuts.my_profile": "Aperir tu profilo",
"keyboard_shortcuts.notifications": "Aperir columna de notificationes",
"keyboard_shortcuts.reply": "Responder al message",
"keyboard_shortcuts.spoilers": "Monstrar/celar le campo CW",
"keyboard_shortcuts.toot": "Initiar un nove message",
"lightbox.close": "Clauder", "lightbox.close": "Clauder",
"lightbox.next": "Sequente", "lightbox.next": "Sequente",
"lightbox.previous": "Precedente",
"link_preview.author": "Per {name}", "link_preview.author": "Per {name}",
"lists.account.add": "Adder al lista", "lists.account.add": "Adder al lista",
"lists.account.remove": "Remover ab le lista",
"lists.delete": "Deler lista",
"lists.edit": "Modificar lista",
"lists.edit.submit": "Cambiar titulo",
"lists.exclusive": "Celar iste messages ab le initio",
"lists.new.create": "Adder lista",
"lists.new.title_placeholder": "Nove titulo del lista",
"lists.replies_policy.title": "Monstrar responsas a:",
"lists.subheading": "Tu listas",
"mute_modal.duration": "Duration", "mute_modal.duration": "Duration",
"mute_modal.hide_notifications": "Celar notificationes de iste usator?", "mute_modal.hide_notifications": "Celar notificationes de iste usator?",
"navigation_bar.about": "A proposito de", "navigation_bar.about": "A proposito de",
"navigation_bar.advanced_interface": "Aperir in un interfacie web avantiate", "navigation_bar.advanced_interface": "Aperir in un interfacie web avantiate",
"navigation_bar.blocks": "Usatores blocate", "navigation_bar.blocks": "Usatores blocate",
"navigation_bar.bookmarks": "Marcapaginas",
"navigation_bar.community_timeline": "Chronologia local",
"navigation_bar.direct": "Mentiones private",
"navigation_bar.discover": "Discoperir",
"navigation_bar.domain_blocks": "Dominios blocate",
"navigation_bar.edit_profile": "Modificar profilo",
"navigation_bar.favourites": "Favoritos", "navigation_bar.favourites": "Favoritos",
"navigation_bar.filters": "Parolas silentiate",
"navigation_bar.lists": "Listas", "navigation_bar.lists": "Listas",
"navigation_bar.logout": "Clauder le session", "navigation_bar.logout": "Clauder le session",
"navigation_bar.mutes": "Usatores silentiate",
"navigation_bar.preferences": "Preferentias", "navigation_bar.preferences": "Preferentias",
"navigation_bar.public_timeline": "Chronologia federate",
"navigation_bar.search": "Cercar",
"navigation_bar.security": "Securitate", "navigation_bar.security": "Securitate",
"notification.update": "{name} modificava un message",
"notifications.clear": "Rader notificationes",
"notifications.column_settings.alert": "Notificationes de scriptorio", "notifications.column_settings.alert": "Notificationes de scriptorio",
"notifications.column_settings.filter_bar.advanced": "Monstrar tote le categorias", "notifications.column_settings.filter_bar.advanced": "Monstrar tote le categorias",
"notifications.column_settings.follow": "Nove sequitores:",
"notifications.column_settings.mention": "Mentiones:",
"notifications.column_settings.push": "Notificationes push",
"notifications.column_settings.sound": "Reproducer sono", "notifications.column_settings.sound": "Reproducer sono",
"notifications.column_settings.status": "Nove messages:",
"notifications.filter.all": "Toto", "notifications.filter.all": "Toto",
"notifications.filter.favourites": "Favoritos",
"notifications.filter.mentions": "Mentiones",
"notifications.grant_permission": "Conceder permission.",
"notifications.group": "{count} notificationes",
"onboarding.compose.template": "Salute #Mastodon!", "onboarding.compose.template": "Salute #Mastodon!",
"onboarding.profile.save_and_continue": "Salvar e continuar", "onboarding.profile.save_and_continue": "Salvar e continuar",
"onboarding.share.title": "Compartir tu profilo" "onboarding.share.next_steps": "Sequente passos possibile:",
"onboarding.share.title": "Compartir tu profilo",
"onboarding.steps.follow_people.title": "Personalisa tu fluxo de initio",
"onboarding.steps.publish_status.title": "Face tu prime message",
"onboarding.steps.setup_profile.title": "Personalisa tu profilo",
"onboarding.steps.share_profile.title": "Compartir tu profilo de Mastodon",
"poll.closed": "Claudite",
"poll.reveal": "Vider le resultatos",
"privacy.change": "Cambiar privacitate del message",
"privacy.private.long": "Visibile solmente pro sequitores",
"privacy.public.long": "Visibile pro totos",
"privacy.public.short": "Public",
"privacy_policy.last_updated": "Ultime actualisation {date}",
"privacy_policy.title": "Politica de confidentialitate",
"relative_time.just_now": "ora",
"relative_time.today": "hodie",
"reply_indicator.cancel": "Cancellar",
"report.block": "Blocar",
"report.categories.other": "Alteres",
"report.category.title_account": "profilo",
"report.category.title_status": "message",
"report.close": "Preste",
"report.mute": "Silentiar",
"report.next": "Sequente",
"report.placeholder": "Commentos additional",
"report.reasons.dislike": "Non me place",
"report_notification.categories.other": "Alteres",
"search.quick_action.go_to_account": "Vader al profilo {x}",
"search.quick_action.go_to_hashtag": "Vader al hashtag {x}",
"search.quick_action.open_url": "Aperir URL in Mastodon",
"search_popout.full_text_search_disabled_message": "Non disponibile sur {domain}.",
"search_popout.language_code": "Codice de lingua ISO",
"search_popout.options": "Optiones de recerca",
"search_popout.quick_actions": "Actiones rapide",
"search_popout.recent": "Recercas recente",
"search_popout.user": "usator",
"search_results.accounts": "Profilos",
"search_results.hashtags": "Hashtags",
"search_results.see_all": "Vider toto",
"search_results.statuses": "Messages",
"server_banner.learn_more": "Apprender plus",
"sign_in_banner.create_account": "Crear un conto",
"sign_in_banner.sign_in": "Initiar le session",
"status.block": "Blocar @{name}",
"status.copy": "Copiar ligamine a message",
"status.delete": "Deler",
"status.direct_indicator": "Mention private",
"status.edit": "Modificar",
"status.filter": "Filtrar iste message",
"status.hide": "Celar le message",
"status.history.created": "create per {name} le {date}",
"status.history.edited": "modificate per {name} le {date}",
"status.media.open": "Clicca pro aperir",
"status.media.show": "Clicca pro monstrar",
"status.more": "Plus",
"status.mute_conversation": "Silentiar conversation",
"status.read_more": "Leger plus",
"status.share": "Compartir",
"status.translate": "Traducer",
"status.translated_from_with": "Traducite ab {lang} usante {provider}",
"tabs_bar.home": "Initio",
"tabs_bar.notifications": "Notificationes",
"timeline_hint.resources.statuses": "Messages ancian",
"trends.trending_now": "Ora in tendentias",
"upload_form.undo": "Deler",
"upload_modal.choose_image": "Seliger un imagine",
"upload_modal.detect_text": "Deteger texto ab un pictura",
"video.close": "Clauder le video",
"video.download": "Discargar le file",
"video.fullscreen": "Schermo plen",
"video.hide": "Celar video",
"video.mute": "Silentiar le sono",
"video.pause": "Pausa",
"video.play": "Reproducer",
"video.unmute": "Non plus silentiar le sono"
} }

View File

@ -32,6 +32,7 @@
"account.featured_tags.last_status_never": "投稿がありません", "account.featured_tags.last_status_never": "投稿がありません",
"account.featured_tags.title": "{name}の注目ハッシュタグ", "account.featured_tags.title": "{name}の注目ハッシュタグ",
"account.follow": "フォロー", "account.follow": "フォロー",
"account.follow_back": "フォローバック",
"account.followers": "フォロワー", "account.followers": "フォロワー",
"account.followers.empty": "まだ誰もフォローしていません。", "account.followers.empty": "まだ誰もフォローしていません。",
"account.followers_counter": "{counter} フォロワー", "account.followers_counter": "{counter} フォロワー",
@ -52,6 +53,7 @@
"account.mute_notifications_short": "通知をオフにする", "account.mute_notifications_short": "通知をオフにする",
"account.mute_short": "ミュート", "account.mute_short": "ミュート",
"account.muted": "ミュート済み", "account.muted": "ミュート済み",
"account.mutual": "相互フォロー中",
"account.no_bio": "説明が提供されていません。", "account.no_bio": "説明が提供されていません。",
"account.open_original_page": "元のページを開く", "account.open_original_page": "元のページを開く",
"account.posts": "投稿", "account.posts": "投稿",
@ -486,7 +488,7 @@
"onboarding.profile.display_name_hint": "フルネーム、あるいは面白い名前など", "onboarding.profile.display_name_hint": "フルネーム、あるいは面白い名前など",
"onboarding.profile.lead": "あとでいつでも修正できますし、設定画面にはこれ以外のカスタマイズ項目もあります。", "onboarding.profile.lead": "あとでいつでも修正できますし、設定画面にはこれ以外のカスタマイズ項目もあります。",
"onboarding.profile.note": "自己紹介", "onboarding.profile.note": "自己紹介",
"onboarding.profile.note_hint": "ほかの人に @言及 したり、#ハッシュタグ を付けたりできます", "onboarding.profile.note_hint": "ほかのユーザーへのメンション (@mention) や、 #ハッシュタグ が使用できます",
"onboarding.profile.save_and_continue": "保存して続ける", "onboarding.profile.save_and_continue": "保存して続ける",
"onboarding.profile.title": "プロフィールの設定", "onboarding.profile.title": "プロフィールの設定",
"onboarding.profile.upload_avatar": "プロフィール画像をアップロード", "onboarding.profile.upload_avatar": "プロフィール画像をアップロード",

View File

@ -501,6 +501,8 @@
"onboarding.steps.setup_profile.title": "Personaliza tu profil", "onboarding.steps.setup_profile.title": "Personaliza tu profil",
"onboarding.steps.share_profile.body": "Informe a tus amigos komo toparte en Mastodon", "onboarding.steps.share_profile.body": "Informe a tus amigos komo toparte en Mastodon",
"onboarding.steps.share_profile.title": "Partaja tu profil de Mastodon", "onboarding.steps.share_profile.title": "Partaja tu profil de Mastodon",
"password_confirmation.exceeds_maxlength": "La konfirmasyon de kod es demaziado lunga",
"password_confirmation.mismatching": "Los dos kodes son desferentes",
"picture_in_picture.restore": "Restora", "picture_in_picture.restore": "Restora",
"poll.closed": "Serrado", "poll.closed": "Serrado",
"poll.refresh": "Arefreska", "poll.refresh": "Arefreska",

View File

@ -606,7 +606,7 @@
"search.quick_action.status_search": "Innlegg som samsvarer med {x}", "search.quick_action.status_search": "Innlegg som samsvarer med {x}",
"search.search_or_paste": "Søk eller lim inn URL", "search.search_or_paste": "Søk eller lim inn URL",
"search_popout.full_text_search_disabled_message": "Ikkje tilgjengeleg på {domain}.", "search_popout.full_text_search_disabled_message": "Ikkje tilgjengeleg på {domain}.",
"search_popout.full_text_search_logged_out_message": "Bare tilgjengelig ved innlogging.", "search_popout.full_text_search_logged_out_message": "Bare tilgjengelig når man er logget inn.",
"search_popout.language_code": "ISO-språkkode", "search_popout.language_code": "ISO-språkkode",
"search_popout.options": "Søkjealternativ", "search_popout.options": "Søkjealternativ",
"search_popout.quick_actions": "Hurtighandlinger", "search_popout.quick_actions": "Hurtighandlinger",

View File

@ -606,7 +606,7 @@
"search.quick_action.status_search": "Innlegg som samsvarer med {x}", "search.quick_action.status_search": "Innlegg som samsvarer med {x}",
"search.search_or_paste": "Søk eller lim inn URL", "search.search_or_paste": "Søk eller lim inn URL",
"search_popout.full_text_search_disabled_message": "Ikke tilgjengelig på {domain}.", "search_popout.full_text_search_disabled_message": "Ikke tilgjengelig på {domain}.",
"search_popout.full_text_search_logged_out_message": "Bare tilgjengelig ved innlogging.", "search_popout.full_text_search_logged_out_message": "Bare tilgjengelig når man er logget inn.",
"search_popout.language_code": "ISO språkkode", "search_popout.language_code": "ISO språkkode",
"search_popout.options": "Alternativer for søk", "search_popout.options": "Alternativer for søk",
"search_popout.quick_actions": "Hurtighandlinger", "search_popout.quick_actions": "Hurtighandlinger",

View File

@ -32,6 +32,7 @@
"account.featured_tags.last_status_never": "Pa postime", "account.featured_tags.last_status_never": "Pa postime",
"account.featured_tags.title": "Hashtagë të zgjedhur të {name}", "account.featured_tags.title": "Hashtagë të zgjedhur të {name}",
"account.follow": "Ndiqeni", "account.follow": "Ndiqeni",
"account.follow_back": "Ndiqe gjithashtu",
"account.followers": "Ndjekës", "account.followers": "Ndjekës",
"account.followers.empty": "Këtë përdorues ende se ndjek kush.", "account.followers.empty": "Këtë përdorues ende se ndjek kush.",
"account.followers_counter": "{count, plural, one {{counter} Ndjekës} other {{counter} Ndjekës}}", "account.followers_counter": "{count, plural, one {{counter} Ndjekës} other {{counter} Ndjekës}}",
@ -52,6 +53,7 @@
"account.mute_notifications_short": "Mos shfaq njoftime", "account.mute_notifications_short": "Mos shfaq njoftime",
"account.mute_short": "Mos i shfaq", "account.mute_short": "Mos i shfaq",
"account.muted": "Heshtuar", "account.muted": "Heshtuar",
"account.mutual": "Reciproke",
"account.no_bio": "Su dha përshkrim.", "account.no_bio": "Su dha përshkrim.",
"account.open_original_page": "Hap faqen origjinale", "account.open_original_page": "Hap faqen origjinale",
"account.posts": "Mesazhe", "account.posts": "Mesazhe",

View File

@ -240,7 +240,7 @@
"empty_column.follow_requests": "Bạn chưa có yêu cầu theo dõi nào.", "empty_column.follow_requests": "Bạn chưa có yêu cầu theo dõi nào.",
"empty_column.followed_tags": "Bạn chưa theo dõi hashtag nào. Khi bạn theo dõi, chúng sẽ hiện lên ở đây.", "empty_column.followed_tags": "Bạn chưa theo dõi hashtag nào. Khi bạn theo dõi, chúng sẽ hiện lên ở đây.",
"empty_column.hashtag": "Chưa có tút nào dùng hashtag này.", "empty_column.hashtag": "Chưa có tút nào dùng hashtag này.",
"empty_column.home": "Trang chính của bạn đang trống! Hãy theo dõi nhiều người hơn để lấp đầy.", "empty_column.home": "Trang ch của bạn đang trống! Hãy theo dõi nhiều người hơn để lấp đầy.",
"empty_column.list": "Chưa có tút. Khi những người trong danh sách này đăng tút mới, chúng sẽ xuất hiện ở đây.", "empty_column.list": "Chưa có tút. Khi những người trong danh sách này đăng tút mới, chúng sẽ xuất hiện ở đây.",
"empty_column.lists": "Bạn chưa tạo danh sách nào.", "empty_column.lists": "Bạn chưa tạo danh sách nào.",
"empty_column.mutes": "Bạn chưa ẩn bất kỳ ai.", "empty_column.mutes": "Bạn chưa ẩn bất kỳ ai.",
@ -336,38 +336,38 @@
"intervals.full.days": "{number, plural, other {# ngày}}", "intervals.full.days": "{number, plural, other {# ngày}}",
"intervals.full.hours": "{number, plural, other {# giờ}}", "intervals.full.hours": "{number, plural, other {# giờ}}",
"intervals.full.minutes": "{number, plural, other {# phút}}", "intervals.full.minutes": "{number, plural, other {# phút}}",
"keyboard_shortcuts.back": "trở lại", "keyboard_shortcuts.back": "quay lại",
"keyboard_shortcuts.blocked": "mở danh sách người đã chặn", "keyboard_shortcuts.blocked": "mở danh sách người đã chặn",
"keyboard_shortcuts.boost": "đăng lại", "keyboard_shortcuts.boost": "đăng lại",
"keyboard_shortcuts.column": "mở các mục", "keyboard_shortcuts.column": "mở các cột",
"keyboard_shortcuts.compose": "mở khung soạn tút", "keyboard_shortcuts.compose": "mở khung soạn tút",
"keyboard_shortcuts.description": "Mô tả", "keyboard_shortcuts.description": "Mô tả",
"keyboard_shortcuts.direct": "mở mục nhắn riêng", "keyboard_shortcuts.direct": "mở nhắn riêng",
"keyboard_shortcuts.down": "di chuyển xuống dưới danh sách", "keyboard_shortcuts.down": "di chuyển xuống dưới danh sách",
"keyboard_shortcuts.enter": "viết tút mới", "keyboard_shortcuts.enter": "mở tút",
"keyboard_shortcuts.favourite": "thích tút", "keyboard_shortcuts.favourite": "thích tút",
"keyboard_shortcuts.favourites": "mở lượt thích", "keyboard_shortcuts.favourites": "mở lượt thích",
"keyboard_shortcuts.federated": "mở mạng liên hợp", "keyboard_shortcuts.federated": "mở mạng liên hợp",
"keyboard_shortcuts.heading": "Danh sách phím tắt", "keyboard_shortcuts.heading": "Danh sách phím tắt",
"keyboard_shortcuts.home": "mở trang chính", "keyboard_shortcuts.home": "mở trang ch",
"keyboard_shortcuts.hotkey": "Phím tắt", "keyboard_shortcuts.hotkey": "Phím tắt",
"keyboard_shortcuts.legend": "hiện bảng hướng dẫn này", "keyboard_shortcuts.legend": "hiện bảng hướng dẫn này",
"keyboard_shortcuts.local": "mở máy chủ của bạn", "keyboard_shortcuts.local": "mở máy chủ của bạn",
"keyboard_shortcuts.mention": "nhắc đến ai đó", "keyboard_shortcuts.mention": "nhắc đến ai đó",
"keyboard_shortcuts.muted": "mở danh sách người đã ẩn", "keyboard_shortcuts.muted": "mở danh sách người đã ẩn",
"keyboard_shortcuts.my_profile": "mở hồ sơ của bạn", "keyboard_shortcuts.my_profile": "mở hồ sơ của bạn",
"keyboard_shortcuts.notifications": "mở mục thông báo", "keyboard_shortcuts.notifications": "mở thông báo",
"keyboard_shortcuts.open_media": "mở ảnh hoặc video", "keyboard_shortcuts.open_media": "mở ảnh hoặc video",
"keyboard_shortcuts.pinned": "mở danh sách tút ghim", "keyboard_shortcuts.pinned": "mở những tút đã ghim",
"keyboard_shortcuts.profile": "mở hồ sơ của người viết tút", "keyboard_shortcuts.profile": "mở trang của người đăng tút",
"keyboard_shortcuts.reply": "trả lời", "keyboard_shortcuts.reply": "trả lời",
"keyboard_shortcuts.requests": "mở danh sách yêu cầu theo dõi", "keyboard_shortcuts.requests": "mở danh sách yêu cầu theo dõi",
"keyboard_shortcuts.search": "mở tìm kiếm", "keyboard_shortcuts.search": "mở tìm kiếm",
"keyboard_shortcuts.spoilers": "hiện/ẩn nội dung nhạy cảm", "keyboard_shortcuts.spoilers": "hiện/ẩn nội dung nhạy cảm",
"keyboard_shortcuts.start": "mở mục \"Dành cho người mới\"", "keyboard_shortcuts.start": "mở \"Dành cho người mới\"",
"keyboard_shortcuts.toggle_hidden": "ẩn/hiện văn bản bên dưới spoil", "keyboard_shortcuts.toggle_hidden": "ẩn/hiện nội dung ẩn",
"keyboard_shortcuts.toggle_sensitivity": "ẩn/hiện ảnh hoặc video", "keyboard_shortcuts.toggle_sensitivity": "ẩn/hiện ảnh hoặc video",
"keyboard_shortcuts.toot": "viết tút mới", "keyboard_shortcuts.toot": "soạn tút mới",
"keyboard_shortcuts.unfocus": "đưa con trỏ ra khỏi ô soạn thảo hoặc ô tìm kiếm", "keyboard_shortcuts.unfocus": "đưa con trỏ ra khỏi ô soạn thảo hoặc ô tìm kiếm",
"keyboard_shortcuts.up": "di chuyển lên trên danh sách", "keyboard_shortcuts.up": "di chuyển lên trên danh sách",
"lightbox.close": "Đóng", "lightbox.close": "Đóng",
@ -404,7 +404,7 @@
"navigation_bar.blocks": "Người đã chặn", "navigation_bar.blocks": "Người đã chặn",
"navigation_bar.bookmarks": "Đã lưu", "navigation_bar.bookmarks": "Đã lưu",
"navigation_bar.community_timeline": "Cộng đồng", "navigation_bar.community_timeline": "Cộng đồng",
"navigation_bar.compose": "Viết tút mới", "navigation_bar.compose": "Soạn tút mới",
"navigation_bar.direct": "Nhắn riêng", "navigation_bar.direct": "Nhắn riêng",
"navigation_bar.discover": "Khám phá", "navigation_bar.discover": "Khám phá",
"navigation_bar.domain_blocks": "Máy chủ đã ẩn", "navigation_bar.domain_blocks": "Máy chủ đã ẩn",
@ -436,7 +436,7 @@
"notification.poll": "Cuộc bình chọn đã kết thúc", "notification.poll": "Cuộc bình chọn đã kết thúc",
"notification.reblog": "{name} đăng lại tút của bạn", "notification.reblog": "{name} đăng lại tút của bạn",
"notification.status": "{name} đăng tút mới", "notification.status": "{name} đăng tút mới",
"notification.update": "{name} đã viết lại một tút", "notification.update": "{name} đã sửa tút",
"notifications.clear": "Xóa hết thông báo", "notifications.clear": "Xóa hết thông báo",
"notifications.clear_confirmation": "Bạn thật sự muốn xóa vĩnh viễn tất cả thông báo của mình?", "notifications.clear_confirmation": "Bạn thật sự muốn xóa vĩnh viễn tất cả thông báo của mình?",
"notifications.column_settings.admin.report": "Báo cáo mới:", "notifications.column_settings.admin.report": "Báo cáo mới:",
@ -477,7 +477,7 @@
"onboarding.action.back": "Quay lại", "onboarding.action.back": "Quay lại",
"onboarding.actions.back": "Quay lại", "onboarding.actions.back": "Quay lại",
"onboarding.actions.go_to_explore": "Xem những gì đang thịnh hành", "onboarding.actions.go_to_explore": "Xem những gì đang thịnh hành",
"onboarding.actions.go_to_home": "Đến trang chính", "onboarding.actions.go_to_home": "Đến trang ch",
"onboarding.compose.template": "Xin chào #Mastodon!", "onboarding.compose.template": "Xin chào #Mastodon!",
"onboarding.follows.empty": "Không có kết quả có thể được hiển thị lúc này. Bạn có thể thử sử dụng tính năng tìm kiếm hoặc duyệt qua trang khám phá để tìm những người theo dõi hoặc thử lại sau.", "onboarding.follows.empty": "Không có kết quả có thể được hiển thị lúc này. Bạn có thể thử sử dụng tính năng tìm kiếm hoặc duyệt qua trang khám phá để tìm những người theo dõi hoặc thử lại sau.",
"onboarding.follows.lead": "Bạn quản lý bảng tin của riêng bạn. Bạn càng theo dõi nhiều người, nó sẽ càng sôi động và thú vị. Để bắt đầu, đây là vài gợi ý:", "onboarding.follows.lead": "Bạn quản lý bảng tin của riêng bạn. Bạn càng theo dõi nhiều người, nó sẽ càng sôi động và thú vị. Để bắt đầu, đây là vài gợi ý:",
@ -501,7 +501,7 @@
"onboarding.start.skip": "Muốn bỏ qua luôn?", "onboarding.start.skip": "Muốn bỏ qua luôn?",
"onboarding.start.title": "Xong rồi bạn!", "onboarding.start.title": "Xong rồi bạn!",
"onboarding.steps.follow_people.body": "Theo dõi những người thú vị trên Mastodon.", "onboarding.steps.follow_people.body": "Theo dõi những người thú vị trên Mastodon.",
"onboarding.steps.follow_people.title": "Cá nhân hóa trang chính", "onboarding.steps.follow_people.title": "Cá nhân hóa trang ch",
"onboarding.steps.publish_status.body": "Chào cộng đồng bằng lời nói, ảnh hoặc video {emoji}", "onboarding.steps.publish_status.body": "Chào cộng đồng bằng lời nói, ảnh hoặc video {emoji}",
"onboarding.steps.publish_status.title": "Đăng tút đầu tiên", "onboarding.steps.publish_status.title": "Đăng tút đầu tiên",
"onboarding.steps.setup_profile.body": "Tạo sự tương tác bằng một hồ sơ hoàn chỉnh.", "onboarding.steps.setup_profile.body": "Tạo sự tương tác bằng một hồ sơ hoàn chỉnh.",
@ -539,7 +539,7 @@
"recommended": "Đề xuất", "recommended": "Đề xuất",
"refresh": "Làm mới", "refresh": "Làm mới",
"regeneration_indicator.label": "Đang tải…", "regeneration_indicator.label": "Đang tải…",
"regeneration_indicator.sublabel": "Trang chính của bạn đang được cập nhật!", "regeneration_indicator.sublabel": "Trang ch của bạn đang được cập nhật!",
"relative_time.days": "{number} ngày", "relative_time.days": "{number} ngày",
"relative_time.full.days": "{number, plural, other {# ngày}}", "relative_time.full.days": "{number, plural, other {# ngày}}",
"relative_time.full.hours": "{number, plural, other {# giờ}}", "relative_time.full.hours": "{number, plural, other {# giờ}}",
@ -591,7 +591,7 @@
"report.thanks.title": "Không muốn xem thứ này?", "report.thanks.title": "Không muốn xem thứ này?",
"report.thanks.title_actionable": "Cảm ơn đã báo cáo, chúng tôi sẽ xem xét kỹ.", "report.thanks.title_actionable": "Cảm ơn đã báo cáo, chúng tôi sẽ xem xét kỹ.",
"report.unfollow": "Bỏ theo dõi @{name}", "report.unfollow": "Bỏ theo dõi @{name}",
"report.unfollow_explanation": "Bạn đang theo dõi người này. Để không thấy tút của họ trên trang chính nữa, hãy bỏ theo dõi.", "report.unfollow_explanation": "Bạn đang theo dõi người này. Để không thấy tút của họ trên trang ch nữa, hãy bỏ theo dõi.",
"report_notification.attached_statuses": "{count, plural, other {{count} tút}} đính kèm", "report_notification.attached_statuses": "{count, plural, other {{count} tút}} đính kèm",
"report_notification.categories.legal": "Pháp lý", "report_notification.categories.legal": "Pháp lý",
"report_notification.categories.other": "Khác", "report_notification.categories.other": "Khác",
@ -692,7 +692,7 @@
"subscribed_languages.lead": "Chỉ các tút đăng bằng các ngôn ngữ đã chọn mới được xuất hiện trên bảng tin của bạn. Không chọn gì cả để đọc tút đăng bằng mọi ngôn ngữ.", "subscribed_languages.lead": "Chỉ các tút đăng bằng các ngôn ngữ đã chọn mới được xuất hiện trên bảng tin của bạn. Không chọn gì cả để đọc tút đăng bằng mọi ngôn ngữ.",
"subscribed_languages.save": "Lưu thay đổi", "subscribed_languages.save": "Lưu thay đổi",
"subscribed_languages.target": "Đổi ngôn ngữ mong muốn cho {target}", "subscribed_languages.target": "Đổi ngôn ngữ mong muốn cho {target}",
"tabs_bar.home": "Trang chính", "tabs_bar.home": "Trang ch",
"tabs_bar.notifications": "Thông báo", "tabs_bar.notifications": "Thông báo",
"time_remaining.days": "{number, plural, other {# ngày}}", "time_remaining.days": "{number, plural, other {# ngày}}",
"time_remaining.hours": "{number, plural, other {# giờ}}", "time_remaining.hours": "{number, plural, other {# giờ}}",

View File

@ -53,7 +53,7 @@
"account.mute_notifications_short": "关闭通知", "account.mute_notifications_short": "关闭通知",
"account.mute_short": "隐藏", "account.mute_short": "隐藏",
"account.muted": "已隐藏", "account.muted": "已隐藏",
"account.mutual": "互相关注", "account.mutual": "互粉好友",
"account.no_bio": "未提供描述。", "account.no_bio": "未提供描述。",
"account.open_original_page": "打开原始页面", "account.open_original_page": "打开原始页面",
"account.posts": "嘟文", "account.posts": "嘟文",
@ -446,7 +446,7 @@
"notifications.column_settings.filter_bar.advanced": "显示所有类别", "notifications.column_settings.filter_bar.advanced": "显示所有类别",
"notifications.column_settings.filter_bar.category": "快速过滤栏", "notifications.column_settings.filter_bar.category": "快速过滤栏",
"notifications.column_settings.filter_bar.show_bar": "显示过滤栏", "notifications.column_settings.filter_bar.show_bar": "显示过滤栏",
"notifications.column_settings.follow": "新关注者", "notifications.column_settings.follow": "新粉丝",
"notifications.column_settings.follow_request": "新关注请求:", "notifications.column_settings.follow_request": "新关注请求:",
"notifications.column_settings.mention": "提及:", "notifications.column_settings.mention": "提及:",
"notifications.column_settings.poll": "投票结果:", "notifications.column_settings.poll": "投票结果:",
@ -700,7 +700,7 @@
"time_remaining.moments": "即将结束", "time_remaining.moments": "即将结束",
"time_remaining.seconds": "剩余 {number, plural, one {# 秒} other {# 秒}}", "time_remaining.seconds": "剩余 {number, plural, one {# 秒} other {# 秒}}",
"timeline_hint.remote_resource_not_displayed": "不会显示来自其它服务器的{resource}", "timeline_hint.remote_resource_not_displayed": "不会显示来自其它服务器的{resource}",
"timeline_hint.resources.followers": "关注者", "timeline_hint.resources.followers": "粉丝",
"timeline_hint.resources.follows": "关注", "timeline_hint.resources.follows": "关注",
"timeline_hint.resources.statuses": "更早的嘟文", "timeline_hint.resources.statuses": "更早的嘟文",
"trends.counter_by_accounts": "过去 {days, plural, other {{days} 天}}有{count, plural, other { {counter} 人}}讨论", "trends.counter_by_accounts": "过去 {days, plural, other {{days} 天}}有{count, plural, other { {counter} 人}}讨论",

View File

@ -1,7 +1,6 @@
import type { Reducer } from '@reduxjs/toolkit';
import { Map as ImmutableMap } from 'immutable'; import { Map as ImmutableMap } from 'immutable';
import type { Reducer } from 'redux';
import { import {
followAccountSuccess, followAccountSuccess,
unfollowAccountSuccess, unfollowAccountSuccess,

View File

@ -1,6 +1,5 @@
import { Record as ImmutableRecord, Stack } from 'immutable';
import type { Reducer } from '@reduxjs/toolkit'; import type { Reducer } from '@reduxjs/toolkit';
import { Record as ImmutableRecord, Stack } from 'immutable';
import { COMPOSE_UPLOAD_CHANGE_SUCCESS } from '../actions/compose'; import { COMPOSE_UPLOAD_CHANGE_SUCCESS } from '../actions/compose';
import type { ModalType } from '../actions/modal'; import type { ModalType } from '../actions/modal';

View File

@ -1,7 +1,6 @@
import { Map as ImmutableMap } from 'immutable';
import { isFulfilled } from '@reduxjs/toolkit'; import { isFulfilled } from '@reduxjs/toolkit';
import type { Reducer } from 'redux'; import type { Reducer } from '@reduxjs/toolkit';
import { Map as ImmutableMap } from 'immutable';
import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships'; import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships';
import type { Account } from 'mastodon/models/account'; import type { Account } from 'mastodon/models/account';

View File

@ -1,5 +1,5 @@
import { createSelector } from '@reduxjs/toolkit';
import { Record as ImmutableRecord } from 'immutable'; import { Record as ImmutableRecord } from 'immutable';
import { createSelector } from 'reselect';
import { accountDefaultValues } from 'mastodon/models/account'; import { accountDefaultValues } from 'mastodon/models/account';
import type { Account, AccountShape } from 'mastodon/models/account'; import type { Account, AccountShape } from 'mastodon/models/account';

View File

@ -1,5 +1,5 @@
import { createSelector } from '@reduxjs/toolkit';
import { List as ImmutableList, Map as ImmutableMap } from 'immutable'; import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import { createSelector } from 'reselect';
import { toServerSideType } from 'mastodon/utils/filters'; import { toServerSideType } from 'mastodon/utils/filters';

View File

@ -1,21 +1,35 @@
import type { AnyAction, Middleware } from 'redux'; import { isAction } from '@reduxjs/toolkit';
import type { Action, Middleware } from '@reduxjs/toolkit';
import type { RootState } from '..'; import type { RootState } from '..';
import { showAlertForError } from '../../actions/alerts'; import { showAlertForError } from '../../actions/alerts';
const defaultFailSuffix = 'FAIL'; const defaultFailSuffix = 'FAIL';
const isFailedAction = new RegExp(`${defaultFailSuffix}$`, 'g');
export const errorsMiddleware: Middleware<unknown, RootState> = interface ActionWithMaybeAlertParams extends Action {
skipAlert?: boolean;
skipNotFound?: boolean;
error?: unknown;
}
function isActionWithmaybeAlertParams(
action: unknown,
): action is ActionWithMaybeAlertParams {
return isAction(action);
}
export const errorsMiddleware: Middleware<Record<string, never>, RootState> =
({ dispatch }) => ({ dispatch }) =>
(next) => (next) =>
(action: AnyAction & { skipAlert?: boolean; skipNotFound?: boolean }) => { (action) => {
if (action.type && !action.skipAlert) { if (
const isFail = new RegExp(`${defaultFailSuffix}$`, 'g'); isActionWithmaybeAlertParams(action) &&
!action.skipAlert &&
if (typeof action.type === 'string' && action.type.match(isFail)) { action.type.match(isFailedAction)
) {
dispatch(showAlertForError(action.error, action.skipNotFound)); dispatch(showAlertForError(action.error, action.skipNotFound));
} }
}
return next(action); return next(action);
}; };

View File

@ -3,9 +3,11 @@ import {
isPending as isThunkActionPending, isPending as isThunkActionPending,
isFulfilled as isThunkActionFulfilled, isFulfilled as isThunkActionFulfilled,
isRejected as isThunkActionRejected, isRejected as isThunkActionRejected,
isAction,
} from '@reduxjs/toolkit'; } from '@reduxjs/toolkit';
import type { Middleware, UnknownAction } from '@reduxjs/toolkit';
import { showLoading, hideLoading } from 'react-redux-loading-bar'; import { showLoading, hideLoading } from 'react-redux-loading-bar';
import type { AnyAction, Middleware } from 'redux';
import type { RootState } from '..'; import type { RootState } from '..';
@ -19,14 +21,28 @@ const defaultTypeSuffixes: Config['promiseTypeSuffixes'] = [
'REJECTED', 'REJECTED',
]; ];
interface ActionWithSkipLoading extends UnknownAction {
skipLoading: boolean;
}
function isActionWithSkipLoading(
action: unknown,
): action is ActionWithSkipLoading {
return (
isAction(action) &&
'skipLoading' in action &&
typeof action.skipLoading === 'boolean'
);
}
export const loadingBarMiddleware = ( export const loadingBarMiddleware = (
config: Config = {}, config: Config = {},
): Middleware<unknown, RootState> => { ): Middleware<{ skipLoading?: boolean }, RootState> => {
const promiseTypeSuffixes = config.promiseTypeSuffixes ?? defaultTypeSuffixes; const promiseTypeSuffixes = config.promiseTypeSuffixes ?? defaultTypeSuffixes;
return ({ dispatch }) => return ({ dispatch }) =>
(next) => (next) =>
(action: AnyAction) => { (action) => {
let isPending = false; let isPending = false;
let isFulfilled = false; let isFulfilled = false;
let isRejected = false; let isRejected = false;
@ -39,7 +55,7 @@ export const loadingBarMiddleware = (
else if (isThunkActionFulfilled(action)) isFulfilled = true; else if (isThunkActionFulfilled(action)) isFulfilled = true;
else if (isThunkActionRejected(action)) isRejected = true; else if (isThunkActionRejected(action)) isRejected = true;
} else if ( } else if (
action.type && isActionWithSkipLoading(action) &&
!action.skipLoading && !action.skipLoading &&
typeof action.type === 'string' typeof action.type === 'string'
) { ) {

View File

@ -1,4 +1,5 @@
import type { Middleware, AnyAction } from 'redux'; import { isAction } from '@reduxjs/toolkit';
import type { Middleware, UnknownAction } from '@reduxjs/toolkit';
import ready from 'mastodon/ready'; import ready from 'mastodon/ready';
import { assetHost } from 'mastodon/utils/config'; import { assetHost } from 'mastodon/utils/config';
@ -10,6 +11,21 @@ interface AudioSource {
type: string; type: string;
} }
interface ActionWithMetaSound extends UnknownAction {
meta: { sound: string };
}
function isActionWithMetaSound(action: unknown): action is ActionWithMetaSound {
return (
isAction(action) &&
'meta' in action &&
typeof action.meta === 'object' &&
!!action.meta &&
'sound' in action.meta &&
typeof action.meta.sound === 'string'
);
}
const createAudio = (sources: AudioSource[]) => { const createAudio = (sources: AudioSource[]) => {
const audio = new Audio(); const audio = new Audio();
sources.forEach(({ type, src }) => { sources.forEach(({ type, src }) => {
@ -34,7 +50,10 @@ const play = (audio: HTMLAudioElement) => {
void audio.play(); void audio.play();
}; };
export const soundsMiddleware = (): Middleware<unknown, RootState> => { export const soundsMiddleware = (): Middleware<
Record<string, never>,
RootState
> => {
const soundCache: Record<string, HTMLAudioElement> = {}; const soundCache: Record<string, HTMLAudioElement> = {};
void ready(() => { void ready(() => {
@ -50,14 +69,14 @@ export const soundsMiddleware = (): Middleware<unknown, RootState> => {
]); ]);
}); });
return () => return () => (next) => (action) => {
(next) => if (isActionWithMetaSound(action)) {
(action: AnyAction & { meta?: { sound?: string } }) => { const sound = action.meta.sound;
const sound = action.meta?.sound;
if (sound && Object.hasOwn(soundCache, sound)) { if (sound && Object.hasOwn(soundCache, sound)) {
play(soundCache[sound]); play(soundCache[sound]);
} }
}
return next(action); return next(action);
}; };

View File

@ -1,7 +1,7 @@
import type { TypedUseSelectorHook } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { createAsyncThunk } from '@reduxjs/toolkit'; import { createAsyncThunk } from '@reduxjs/toolkit';
import type { TypedUseSelectorHook } from 'react-redux';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { useDispatch, useSelector } from 'react-redux';
import type { AppDispatch, RootState } from './store'; import type { AppDispatch, RootState } from './store';

View File

@ -10,7 +10,7 @@ class ContentSecurityPolicy
end end
def media_hosts def media_hosts
[assets_host, cdn_host_value].concat(extra_data_hosts).compact [assets_host, cdn_host_value, paperclip_root_url].concat(extra_data_hosts).compact
end end
private private
@ -27,6 +27,15 @@ class ContentSecurityPolicy
s3_alias_host || s3_cloudfront_host || azure_alias_host || s3_hostname_host s3_alias_host || s3_cloudfront_host || azure_alias_host || s3_hostname_host
end end
def paperclip_root_url
root_url = ENV.fetch('PAPERCLIP_ROOT_URL', nil)
return if root_url.blank?
(Addressable::URI.parse(assets_host) + root_url).tap do |uri|
uri.path += '/' unless uri.path.blank? || uri.path.end_with?('/')
end.to_s
end
def url_from_base_host def url_from_base_host
host_to_url(base_host) host_to_url(base_host)
end end

View File

@ -66,16 +66,6 @@ class EmojiFormatter
@emoji_map ||= custom_emojis.each_with_object({}) { |e, h| h[e.shortcode] = [full_asset_url(e.image.url), full_asset_url(e.image.url(:static))] } @emoji_map ||= custom_emojis.each_with_object({}) { |e, h| h[e.shortcode] = [full_asset_url(e.image.url), full_asset_url(e.image.url(:static))] }
end end
def count_tag_nesting(tag)
if tag[1] == '/'
-1
elsif tag[-2] == '/'
0
else
1
end
end
def tag_for_emoji(shortcode, emoji) def tag_for_emoji(shortcode, emoji)
return content_tag(:span, ":#{shortcode}:", translate: 'no') if raw_shortcode? return content_tag(:span, ":#{shortcode}:", translate: 'no') if raw_shortcode?

View File

@ -112,6 +112,8 @@ class Account < ApplicationRecord
validates :shared_inbox_url, absence: true, if: :local?, on: :create validates :shared_inbox_url, absence: true, if: :local?, on: :create
validates :followers_url, absence: true, if: :local?, on: :create validates :followers_url, absence: true, if: :local?, on: :create
normalizes :username, with: ->(username) { username.squish }
scope :remote, -> { where.not(domain: nil) } scope :remote, -> { where.not(domain: nil) }
scope :local, -> { where(domain: nil) } scope :local, -> { where(domain: nil) }
scope :partitioned, -> { order(Arel.sql('row_number() over (partition by domain)')) } scope :partitioned, -> { order(Arel.sql('row_number() over (partition by domain)')) }
@ -133,7 +135,7 @@ class Account < ApplicationRecord
scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).joins(:account_stat) } scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).joins(:account_stat) }
scope :followable_by, ->(account) { joins(arel_table.join(Follow.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(Follow.arel_table[:target_account_id]).and(Follow.arel_table[:account_id].eq(account.id))).join_sources).where(Follow.arel_table[:id].eq(nil)).joins(arel_table.join(FollowRequest.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(FollowRequest.arel_table[:target_account_id]).and(FollowRequest.arel_table[:account_id].eq(account.id))).join_sources).where(FollowRequest.arel_table[:id].eq(nil)) } scope :followable_by, ->(account) { joins(arel_table.join(Follow.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(Follow.arel_table[:target_account_id]).and(Follow.arel_table[:account_id].eq(account.id))).join_sources).where(Follow.arel_table[:id].eq(nil)).joins(arel_table.join(FollowRequest.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(FollowRequest.arel_table[:target_account_id]).and(FollowRequest.arel_table[:account_id].eq(account.id))).join_sources).where(FollowRequest.arel_table[:id].eq(nil)) }
scope :by_recent_status, -> { includes(:account_stat).merge(AccountStat.order('last_status_at DESC NULLS LAST')).references(:account_stat) } scope :by_recent_status, -> { includes(:account_stat).merge(AccountStat.order('last_status_at DESC NULLS LAST')).references(:account_stat) }
scope :by_recent_sign_in, -> { order(Arel.sql('users.current_sign_in_at DESC NULLS LAST')) } scope :by_recent_activity, -> { left_joins(:user, :account_stat).order(coalesced_activity_timestamps.desc).order(id: :desc) }
scope :popular, -> { order('account_stats.followers_count desc') } scope :popular, -> { order('account_stats.followers_count desc') }
scope :by_domain_and_subdomains, ->(domain) { where(domain: Instance.by_domain_and_subdomains(domain).select(:domain)) } scope :by_domain_and_subdomains, ->(domain) { where(domain: Instance.by_domain_and_subdomains(domain).select(:domain)) }
scope :not_excluded_by_account, ->(account) { where.not(id: account.excluded_from_timeline_account_ids) } scope :not_excluded_by_account, ->(account) { where.not(id: account.excluded_from_timeline_account_ids) }
@ -440,6 +442,14 @@ class Account < ApplicationRecord
DeliveryFailureTracker.without_unavailable(urls) DeliveryFailureTracker.without_unavailable(urls)
end end
def coalesced_activity_timestamps
Arel.sql(
<<~SQL.squish
COALESCE(users.current_sign_in_at, account_stats.last_status_at, to_timestamp(0))
SQL
)
end
def from_text(text) def from_text(text)
return [] if text.blank? return [] if text.blank?
@ -473,7 +483,6 @@ class Account < ApplicationRecord
end end
before_validation :prepare_contents, if: :local? before_validation :prepare_contents, if: :local?
before_validation :prepare_username, on: :create
before_create :generate_keys before_create :generate_keys
before_destroy :clean_feed_manager before_destroy :clean_feed_manager
@ -491,10 +500,6 @@ class Account < ApplicationRecord
note&.strip! note&.strip!
end end
def prepare_username
username&.squish!
end
def generate_keys def generate_keys
return unless local? && private_key.blank? && public_key.blank? return unless local? && private_key.blank? && public_key.blank?

View File

@ -104,15 +104,7 @@ class AccountFilter
def order_scope(value) def order_scope(value)
case value.to_s case value.to_s
when 'active' when 'active'
accounts_with_users Account.by_recent_activity
.left_joins(:account_stat)
.order(
Arel.sql(
<<~SQL.squish
COALESCE(users.current_sign_in_at, account_stats.last_status_at, to_timestamp(0)) DESC, accounts.id DESC
SQL
)
)
when 'recent' when 'recent'
Account.recent Account.recent
else else

View File

@ -20,9 +20,8 @@
class Announcement < ApplicationRecord class Announcement < ApplicationRecord
scope :unpublished, -> { where(published: false) } scope :unpublished, -> { where(published: false) }
scope :published, -> { where(published: true) } scope :published, -> { where(published: true) }
scope :without_muted, ->(account) { joins("LEFT OUTER JOIN announcement_mutes ON announcement_mutes.announcement_id = announcements.id AND announcement_mutes.account_id = #{account.id}").where(announcement_mutes: { id: nil }) } scope :chronological, -> { order(coalesced_chronology_timestamps.asc) }
scope :chronological, -> { order(Arel.sql('COALESCE(announcements.starts_at, announcements.scheduled_at, announcements.published_at, announcements.created_at) ASC')) } scope :reverse_chronological, -> { order(coalesced_chronology_timestamps.desc) }
scope :reverse_chronological, -> { order(Arel.sql('COALESCE(announcements.starts_at, announcements.scheduled_at, announcements.published_at, announcements.created_at) DESC')) }
has_many :announcement_mutes, dependent: :destroy has_many :announcement_mutes, dependent: :destroy
has_many :announcement_reactions, dependent: :destroy has_many :announcement_reactions, dependent: :destroy
@ -33,6 +32,16 @@ class Announcement < ApplicationRecord
before_validation :set_published, on: :create before_validation :set_published, on: :create
class << self
def coalesced_chronology_timestamps
Arel.sql(
<<~SQL.squish
COALESCE(announcements.starts_at, announcements.scheduled_at, announcements.published_at, announcements.created_at)
SQL
)
end
end
def to_log_human_identifier def to_log_human_identifier
text text
end end
@ -45,10 +54,6 @@ class Announcement < ApplicationRecord
update!(published: false, scheduled_at: nil) update!(published: false, scheduled_at: nil)
end end
def time_range?
starts_at? && ends_at?
end
def mentions def mentions
@mentions ||= Account.from_text(text) @mentions ||= Account.from_text(text)
end end

View File

@ -11,11 +11,12 @@ module Attachmentable
# For some file extensions, there exist different content # For some file extensions, there exist different content
# type variants, and browsers often send the wrong one, # type variants, and browsers often send the wrong one,
# for example, sending an audio .ogg file as video/ogg, # for example, sending an audio .ogg file as video/ogg,
# likewise, MimeMagic also misreports them as such. For # likewise, kt-paperclip also misreports them as such. For
# those files, it is necessary to use the output of the # those files, it is necessary to use the output of the
# `file` utility instead # `file` utility instead
INCORRECT_CONTENT_TYPES = %w( INCORRECT_CONTENT_TYPES = %w(
audio/vorbis audio/vorbis
audio/opus
video/ogg video/ogg
video/webm video/webm
).freeze ).freeze

View File

@ -42,7 +42,7 @@ class CustomEmoji < ApplicationRecord
has_attached_file :image, styles: { static: { format: 'png', convert_options: '-coalesce +profile "!icc,*" +set date:modify +set date:create +set date:timestamp' } }, validate_media_type: false has_attached_file :image, styles: { static: { format: 'png', convert_options: '-coalesce +profile "!icc,*" +set date:modify +set date:create +set date:timestamp' } }, validate_media_type: false
before_validation :downcase_domain normalizes :domain, with: ->(domain) { domain.downcase }
validates_attachment :image, content_type: { content_type: IMAGE_MIME_TYPES }, presence: true validates_attachment :image, content_type: { content_type: IMAGE_MIME_TYPES }, presence: true
validates_attachment_size :image, less_than: LIMIT, unless: :local? validates_attachment_size :image, less_than: LIMIT, unless: :local?
@ -89,7 +89,7 @@ class CustomEmoji < ApplicationRecord
end end
def search(shortcode) def search(shortcode)
where('"custom_emojis"."shortcode" ILIKE ?', "%#{shortcode}%") where(arel_table[:shortcode].matches("%#{sanitize_sql_like(shortcode)}%"))
end end
end end
@ -98,8 +98,4 @@ class CustomEmoji < ApplicationRecord
def remove_entity_cache def remove_entity_cache
Rails.cache.delete(EntityCache.instance.to_key(:emoji, shortcode, domain)) Rails.cache.delete(EntityCache.instance.to_key(:emoji, shortcode, domain))
end end
def downcase_domain
self.domain = domain.downcase unless domain.nil?
end
end end

View File

@ -31,7 +31,7 @@ class CustomEmojiFilter
def scope_for(key, value) def scope_for(key, value)
case key.to_s case key.to_s
when 'local' when 'local'
CustomEmoji.local.left_joins(:category).reorder(Arel.sql('custom_emoji_categories.name ASC NULLS FIRST, custom_emojis.shortcode ASC')) CustomEmoji.local.left_joins(:category).reorder(CustomEmojiCategory.arel_table[:name].asc.nulls_first).order(shortcode: :asc)
when 'remote' when 'remote'
CustomEmoji.remote CustomEmoji.remote
when 'by_domain' when 'by_domain'

View File

@ -17,23 +17,8 @@
class CustomFilter < ApplicationRecord class CustomFilter < ApplicationRecord
self.ignored_columns += %w(whole_word irreversible) self.ignored_columns += %w(whole_word irreversible)
# NOTE: We previously used `alias_attribute` but this does not play nicely alias_attribute :title, :phrase
# with cache alias_attribute :filter_action, :action
def title
phrase
end
def title=(value)
self.phrase = value
end
def filter_action
action
end
def filter_action=(value)
self.action = value
end
VALID_CONTEXTS = %w( VALID_CONTEXTS = %w(
home home
@ -143,6 +128,10 @@ class CustomFilter < ApplicationRecord
end end
def context_must_be_valid def context_must_be_valid
errors.add(:context, I18n.t('filters.errors.invalid_context')) if context.empty? || context.any? { |c| !VALID_CONTEXTS.include?(c) } errors.add(:context, I18n.t('filters.errors.invalid_context')) if invalid_context_value?
end
def invalid_context_value?
context.blank? || context.difference(VALID_CONTEXTS).any?
end end
end end

View File

@ -17,15 +17,7 @@ class CustomFilterKeyword < ApplicationRecord
validates :keyword, presence: true validates :keyword, presence: true
# NOTE: We previously used `alias_attribute` but this does not play nicely alias_attribute :phrase, :keyword
# with cache
def phrase
keyword
end
def phrase=(value)
self.keyword = value
end
before_save :prepare_cache_invalidation! before_save :prepare_cache_invalidation!
before_destroy :prepare_cache_invalidation! before_destroy :prepare_cache_invalidation!

View File

@ -31,7 +31,7 @@ class DomainBlock < ApplicationRecord
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) } scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
scope :with_user_facing_limitations, -> { where(severity: [:silence, :suspend]) } scope :with_user_facing_limitations, -> { where(severity: [:silence, :suspend]) }
scope :with_limitations, -> { where(severity: [:silence, :suspend]).or(where(reject_media: true)) } scope :with_limitations, -> { where(severity: [:silence, :suspend]).or(where(reject_media: true)) }
scope :by_severity, -> { order(Arel.sql('(CASE severity WHEN 0 THEN 1 WHEN 1 THEN 2 WHEN 2 THEN 0 END), domain')) } scope :by_severity, -> { in_order_of(:severity, %w(noop silence suspend)).order(:domain) }
def to_log_human_identifier def to_log_human_identifier
domain domain
@ -85,11 +85,6 @@ class DomainBlock < ApplicationRecord
(reject_media || !other_block.reject_media) && (reject_reports || !other_block.reject_reports) (reject_media || !other_block.reject_media) && (reject_reports || !other_block.reject_reports)
end end
def affected_accounts_count
scope = suspend? ? accounts.where(suspended_at: created_at) : accounts.where(silenced_at: created_at)
scope.count
end
def public_domain def public_domain
return domain unless obfuscate? return domain unless obfuscate?

View File

@ -9,6 +9,7 @@
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# parent_id :bigint(8) # parent_id :bigint(8)
# allow_with_approval :boolean default(FALSE), not null
# #
class EmailDomainBlock < ApplicationRecord class EmailDomainBlock < ApplicationRecord
@ -42,8 +43,8 @@ class EmailDomainBlock < ApplicationRecord
@attempt_ip = attempt_ip @attempt_ip = attempt_ip
end end
def match? def match?(...)
blocking? || invalid_uri? blocking?(...) || invalid_uri?
end end
private private
@ -52,8 +53,8 @@ class EmailDomainBlock < ApplicationRecord
@uris.any?(&:nil?) @uris.any?(&:nil?)
end end
def blocking? def blocking?(allow_with_approval: false)
blocks = EmailDomainBlock.where(domain: domains_with_variants).order(Arel.sql('char_length(domain) desc')) blocks = EmailDomainBlock.where(domain: domains_with_variants, allow_with_approval: allow_with_approval).order(Arel.sql('char_length(domain) desc'))
blocks.each { |block| block.history.add(@attempt_ip) } if @attempt_ip.present? blocks.each { |block| block.history.add(@attempt_ip) } if @attempt_ip.present?
blocks.any? blocks.any?
end end
@ -86,4 +87,8 @@ class EmailDomainBlock < ApplicationRecord
def self.block?(domain_or_domains, attempt_ip: nil) def self.block?(domain_or_domains, attempt_ip: nil)
Matcher.new(domain_or_domains, attempt_ip: attempt_ip).match? Matcher.new(domain_or_domains, attempt_ip: attempt_ip).match?
end end
def self.requires_approval?(domain_or_domains, attempt_ip: nil)
Matcher.new(domain_or_domains, attempt_ip: attempt_ip).match?(allow_with_approval: true)
end
end end

View File

@ -111,8 +111,6 @@ class Status < ApplicationRecord
scope :without_reblogs, -> { where(statuses: { reblog_of_id: nil }) } scope :without_reblogs, -> { where(statuses: { reblog_of_id: nil }) }
scope :with_public_visibility, -> { where(visibility: :public) } scope :with_public_visibility, -> { where(visibility: :public) }
scope :tagged_with, ->(tag_ids) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag_ids }) } scope :tagged_with, ->(tag_ids) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag_ids }) }
scope :excluding_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced_at: nil }) }
scope :including_silenced_accounts, -> { left_outer_joins(:account).where.not(accounts: { silenced_at: nil }) }
scope :not_excluded_by_account, ->(account) { where.not(account_id: account.excluded_from_timeline_account_ids) } scope :not_excluded_by_account, ->(account) { where.not(account_id: account.excluded_from_timeline_account_ids) }
scope :not_domain_blocked_by_account, ->(account) { account.excluded_from_timeline_domains.blank? ? left_outer_joins(:account) : left_outer_joins(:account).where('accounts.domain IS NULL OR accounts.domain NOT IN (?)', account.excluded_from_timeline_domains) } scope :not_domain_blocked_by_account, ->(account) { account.excluded_from_timeline_domains.blank? ? left_outer_joins(:account) : left_outer_joins(:account).where('accounts.domain IS NULL OR accounts.domain NOT IN (?)', account.excluded_from_timeline_domains) }
scope :tagged_with_all, lambda { |tag_ids| scope :tagged_with_all, lambda { |tag_ids|

View File

@ -117,7 +117,6 @@ class User < ApplicationRecord
scope :active, -> { confirmed.where(arel_table[:current_sign_in_at].gteq(ACTIVE_DURATION.ago)).joins(:account).where(accounts: { suspended_at: nil }) } scope :active, -> { confirmed.where(arel_table[:current_sign_in_at].gteq(ACTIVE_DURATION.ago)).joins(:account).where(accounts: { suspended_at: nil }) }
scope :matches_email, ->(value) { where(arel_table[:email].matches("#{value}%")) } scope :matches_email, ->(value) { where(arel_table[:email].matches("#{value}%")) }
scope :matches_ip, ->(value) { left_joins(:ips).where('user_ips.ip <<= ?', value).group('users.id') } scope :matches_ip, ->(value) { left_joins(:ips).where('user_ips.ip <<= ?', value).group('users.id') }
scope :emailable, -> { confirmed.enabled.joins(:account).merge(Account.searchable) }
before_validation :sanitize_languages before_validation :sanitize_languages
before_validation :sanitize_role before_validation :sanitize_role
@ -418,7 +417,7 @@ class User < ApplicationRecord
def set_approved def set_approved
self.approved = begin self.approved = begin
if sign_up_from_ip_requires_approval? if sign_up_from_ip_requires_approval? || sign_up_email_requires_approval?
false false
else else
open_registrations? || valid_invitation? || external? open_registrations? || valid_invitation? || external?
@ -430,6 +429,12 @@ class User < ApplicationRecord
!sign_up_ip.nil? && IpBlock.where(severity: :sign_up_requires_approval).where('ip >>= ?', sign_up_ip.to_s).exists? !sign_up_ip.nil? && IpBlock.where(severity: :sign_up_requires_approval).where('ip >>= ?', sign_up_ip.to_s).exists?
end end
def sign_up_email_requires_approval?
return false unless email.present? || unconfirmed_email.present?
EmailDomainBlock.requires_approval?(email.presence || unconfirmed_email, attempt_ip: sign_up_ip)
end
def open_registrations? def open_registrations?
Setting.registrations_mode == 'open' Setting.registrations_mode == 'open'
end end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class REST::Admin::EmailDomainBlockSerializer < ActiveModel::Serializer class REST::Admin::EmailDomainBlockSerializer < ActiveModel::Serializer
attributes :id, :domain, :created_at, :history attributes :id, :domain, :created_at, :history, :allow_with_approval
def id def id
object.id.to_s object.id.to_s

View File

@ -47,6 +47,7 @@ class EmailMxValidator < ActiveModel::Validator
dns.timeouts = 5 dns.timeouts = 5
records = dns.getresources(domain, Resolv::DNS::Resource::IN::MX).to_a.map { |e| e.exchange.to_s } records = dns.getresources(domain, Resolv::DNS::Resource::IN::MX).to_a.map { |e| e.exchange.to_s }
next if records == [''] # This domain explicitly rejects emails
([domain] + records).uniq.each do |hostname| ([domain] + records).uniq.each do |hostname|
ips.concat(dns.getresources(hostname, Resolv::DNS::Resource::IN::A).to_a.map { |e| e.address.to_s }) ips.concat(dns.getresources(hostname, Resolv::DNS::Resource::IN::A).to_a.map { |e| e.address.to_s })

View File

@ -12,3 +12,7 @@
· ·
= t('admin.email_domain_blocks.attempts_over_week', count: email_domain_block.history.reduce(0) { |sum, day| sum + day.accounts }) = t('admin.email_domain_blocks.attempts_over_week', count: email_domain_block.history.reduce(0) { |sum, day| sum + day.accounts })
- if email_domain_block.allow_with_approval?
·
= t('admin.email_domain_blocks.allow_registrations_with_approval')

View File

@ -7,6 +7,9 @@
.fields-group .fields-group
= f.input :domain, wrapper: :with_block_label, label: t('admin.email_domain_blocks.domain'), input_html: { readonly: defined?(@resolved_records) } = f.input :domain, wrapper: :with_block_label, label: t('admin.email_domain_blocks.domain'), input_html: { readonly: defined?(@resolved_records) }
.fields-group
= f.input :allow_with_approval, wrapper: :with_label, hint: false, label: I18n.t('admin.email_domain_blocks.allow_registrations_with_approval')
- if defined?(@resolved_records) - if defined?(@resolved_records)
%p.hint= t('admin.email_domain_blocks.resolved_dns_records_hint_html') %p.hint= t('admin.email_domain_blocks.resolved_dns_records_hint_html')

View File

@ -35,7 +35,13 @@
- if SoftwareUpdate.check_enabled? && current_user.can?(:view_devops) - if SoftwareUpdate.check_enabled? && current_user.can?(:view_devops)
.fields-group .fields-group
= ff.input :'notification_emails.software_updates', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.software_updates.label'), collection: %w(none critical patch all), label_method: ->(setting) { I18n.t("simple_form.labels.notification_emails.software_updates.#{setting}") }, include_blank: false, hint: false = ff.input :'notification_emails.software_updates',
collection: %w(none critical patch all),
hint: false,
include_blank: false,
label_method: ->(setting) { I18n.t("simple_form.labels.notification_emails.software_updates.#{setting}") },
label: I18n.t('simple_form.labels.notification_emails.software_updates.label'),
wrapper: :with_label
%h4= t 'notifications.other_settings' %h4= t 'notifications.other_settings'

View File

@ -63,6 +63,8 @@ module Mastodon
# Initialize configuration defaults for originally generated Rails version. # Initialize configuration defaults for originally generated Rails version.
config.load_defaults 7.0 config.load_defaults 7.0
config.active_record.marshalling_format_version = 7.1
# Please, add to the `ignore` list any other `lib` subdirectories that do # Please, add to the `ignore` list any other `lib` subdirectories that do
# not contain `.rb` files, or that should not be reloaded or eager loaded. # not contain `.rb` files, or that should not be reloaded or eager loaded.
# Common ones are `templates`, `generators`, or `middleware`, for example. # Common ones are `templates`, `generators`, or `middleware`, for example.

View File

@ -1,10 +0,0 @@
# frozen_string_literal: true
# TODO
# The Rails 7.0 framework default here is to set this true. However, we have a
# location in devise that redirects where we don't have an easy ability to
# override a method or set a config option, but where the redirect does not
# provide this option.
# https://github.com/heartcombo/devise/blob/v4.9.2/app/controllers/devise/confirmations_controller.rb#L28
# Once a solution is found, this line can be removed.
Rails.application.config.action_controller.raise_on_open_redirects = false

View File

@ -158,15 +158,6 @@ Rails.application.config.add_autoload_paths_to_load_path = false
# rather than to rely on a global default. # rather than to rely on a global default.
# Rails.application.config.active_record.default_column_serializer = nil # Rails.application.config.active_record.default_column_serializer = nil
# Enable a performance optimization that serializes Active Record models
# in a faster and more compact way.
#
# To perform a rolling deploy of a Rails 7.1 upgrade, wherein servers that have
# not yet been upgraded must be able to read caches from upgraded servers,
# leave this optimization off on the first deploy, then enable it on a
# subsequent deploy.
# Rails.application.config.active_record.marshalling_format_version = 7.1
# Run `after_commit` and `after_*_commit` callbacks in the order they are defined in a model. # Run `after_commit` and `after_*_commit` callbacks in the order they are defined in a model.
# This matches the behaviour of all other callbacks. # This matches the behaviour of all other callbacks.
# In previous versions of Rails, they ran in the inverse order. # In previous versions of Rails, they ran in the inverse order.

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
# TODO
# Starting with Rails 7.0, the framework default here is to set this true.
# However, we have a location in devise that redirects where we don't have an
# easy ability to override the method or set a config option, and where the
# redirect does not supply this option itself.
# https://github.com/heartcombo/devise/blob/v4.9.2/app/controllers/devise/confirmations_controller.rb#L28
# Once a solution is found, this line can be removed.
Rails.application.config.action_controller.raise_on_open_redirects = false

View File

@ -3,6 +3,7 @@ ia:
activerecord: activerecord:
attributes: attributes:
user: user:
email: Adresse de e-mail
password: Contrasigno password: Contrasigno
user/account: user/account:
username: Nomine de usator username: Nomine de usator

View File

@ -19,6 +19,7 @@ ie:
account: account:
attributes: attributes:
username: username:
invalid: deve contener solmen lítteres, númeres e sublineas
reserved: es reservat reserved: es reservat
admin/webhook: admin/webhook:
attributes: attributes:
@ -39,6 +40,7 @@ ie:
user: user:
attributes: attributes:
email: email:
blocked: usa un ne-permisset provisor de e-posta
unreachable: sembla ne exister unreachable: sembla ne exister
role_id: role_id:
elevated: ne posse esser plu alt quam tui actual rol elevated: ne posse esser plu alt quam tui actual rol

View File

@ -439,6 +439,7 @@ be:
view: Праглядзець новы блок дамену view: Праглядзець новы блок дамену
email_domain_blocks: email_domain_blocks:
add_new: Дадаць add_new: Дадаць
allow_registrations_with_approval: Дазволіць рэгістрацыю з дазволам
attempts_over_week: attempts_over_week:
few: "%{count} спробы рэгіістрацыі за апошні тыдзень" few: "%{count} спробы рэгіістрацыі за апошні тыдзень"
many: "%{count} спроб рэгіістрацыі за апошні тыдзень" many: "%{count} спроб рэгіістрацыі за апошні тыдзень"

View File

@ -425,6 +425,7 @@ bg:
view: Преглед на блокиране на домейн view: Преглед на блокиране на домейн
email_domain_blocks: email_domain_blocks:
add_new: Добавяне на ново add_new: Добавяне на ново
allow_registrations_with_approval: Позволяване на регистрации с одобрение
attempts_over_week: attempts_over_week:
one: "%{count} опит за изминалата седмица" one: "%{count} опит за изминалата седмица"
other: "%{count} опита за регистрация през изминалата седмица" other: "%{count} опита за регистрация през изминалата седмица"

View File

@ -40,8 +40,10 @@ br:
change_role: change_role:
no_role: Roll ebet no_role: Roll ebet
confirm: Kadarnaat confirm: Kadarnaat
confirmed: Kadarnaet
confirming: O kadarnaat confirming: O kadarnaat
custom: Personelaet custom: Personelaet
delete: Dilemel ar roadennoù
deleted: Dilamet deleted: Dilamet
demote: Argilañ demote: Argilañ
disable: Skornañ disable: Skornañ
@ -61,6 +63,7 @@ br:
all: Pep tra all: Pep tra
local: Lec'hel local: Lec'hel
remote: A-bell remote: A-bell
media_attachments: Restroù media stag
moderation: moderation:
active: Oberiant active: Oberiant
all: Pep tra all: Pep tra
@ -68,6 +71,7 @@ br:
silenced: Bevennet silenced: Bevennet
suspended: Astalet suspended: Astalet
title: Habaskadur title: Habaskadur
most_recent_activity: Obererezh nevesañ
perform_full_suspension: Astalañ perform_full_suspension: Astalañ
promote: Brudañ promote: Brudañ
protocol: Komenad protocol: Komenad
@ -76,15 +80,19 @@ br:
remove_header: Dilemel an talbenn remove_header: Dilemel an talbenn
reset: Adderaouekaat reset: Adderaouekaat
reset_password: Adderaouekaat ar ger-tremen reset_password: Adderaouekaat ar ger-tremen
resubscribe: Adkoumanantiñ
role: Roll role: Roll
search: Klask search: Klask
security: Surentez
silence: Bevenniñ silence: Bevenniñ
silenced: Bevennet silenced: Bevennet
statuses: Toudoù statuses: Toudoù
subscribe: Koumanantiñ
suspend: Astalañ suspend: Astalañ
suspended: Astalet suspended: Astalet
title: Kontoù title: Kontoù
undo_silenced: Dizober ar bevennañ undo_silenced: Dizober ar bevennañ
unsubscribe: Digoumanantiñ
username: Anv username: Anv
warn: Diwall warn: Diwall
web: Web web: Web
@ -167,6 +175,7 @@ br:
title: Habaskadur title: Habaskadur
purge: Spurjañ purge: Spurjañ
title: Kevread title: Kevread
total_storage: Restroù media stag
invites: invites:
filter: filter:
all: Pep tra all: Pep tra
@ -265,6 +274,8 @@ br:
tags: tags:
dashboard: dashboard:
tag_uses_measure: implijoù hollek tag_uses_measure: implijoù hollek
not_usable: N'haller ket en implijout
title: Hashtagoù diouzh ar c'hiz
title: Luskadoù title: Luskadoù
warning_presets: warning_presets:
add_new: Ouzhpenniñ unan nevez add_new: Ouzhpenniñ unan nevez
@ -281,6 +292,9 @@ br:
new_appeal: new_appeal:
actions: actions:
none: ur c'hemenn diwall none: ur c'hemenn diwall
new_trends:
new_trending_tags:
title: Hashtagoù diouzh ar c'hiz
appearance: appearance:
discovery: Dizoloadur discovery: Dizoloadur
application_mailer: application_mailer:
@ -289,6 +303,8 @@ br:
auth: auth:
delete_account: Dilemel ar gont delete_account: Dilemel ar gont
delete_account_html: Ma fell deoc'h dilemel ho kont e c'hellit <a href="%{path}">klikañ amañ</a>. Goulennet e vo ganeoc'h kadarnaat an obererezh. delete_account_html: Ma fell deoc'h dilemel ho kont e c'hellit <a href="%{path}">klikañ amañ</a>. Goulennet e vo ganeoc'h kadarnaat an obererezh.
description:
prefix_invited_by_user: Pedet oc'h gant @%{name} da zont e-barzh ar servijer Mastodon-mañ!
login: Mont tre login: Mont tre
logout: Digennaskañ logout: Digennaskañ
migrate_account_html: Ma fell deoc'h adkas ar gont-mañ war-zu unan all e c'hellit <a href="%{path}">arventenniñ an dra-se amañ</a>. migrate_account_html: Ma fell deoc'h adkas ar gont-mañ war-zu unan all e c'hellit <a href="%{path}">arventenniñ an dra-se amañ</a>.
@ -383,6 +399,10 @@ br:
table: table:
uses: Implijoù uses: Implijoù
title: Pediñ tud title: Pediñ tud
media_attachments:
validations:
images_and_video: N'haller stagañ ur video ouzh un embannadur a zo fotoioù gantañ dija
too_many: N'haller ket stagañ muioc'h eget 4 restr
migrations: migrations:
incoming_migrations_html: Evit dilojañ ur gont all da homañ e rankit <a href="%{path}">sevel un alias</a> da gentañ. incoming_migrations_html: Evit dilojañ ur gont all da homañ e rankit <a href="%{path}">sevel un alias</a> da gentañ.
moderation: moderation:
@ -417,13 +437,16 @@ br:
next: Da-heul next: Da-heul
older: Koshoc'h older: Koshoc'h
prev: A-raok prev: A-raok
polls:
errors:
self_vote: N'hallit ket votiñ en ho sontadegoù deoc'h-c'hwi
preferences: preferences:
other: All other: All
posting_defaults: Arventennoù embann dre ziouer posting_defaults: Arventennoù embann dre ziouer
relationships: relationships:
dormant: O kousket dormant: O kousket
followers: Heulier·ezed·ien followers: Heulier·ezed·ien
following: O heuliañ following: Koumanantoù
invited: Pedet invited: Pedet
moved: Dilojet moved: Dilojet
mutual: Kenetre mutual: Kenetre
@ -463,6 +486,7 @@ br:
account_settings: Arventennoù ar gont account_settings: Arventennoù ar gont
development: Diorren development: Diorren
edit_profile: Kemmañ ar profil edit_profile: Kemmañ ar profil
featured_tags: Hashtagoù pennañ
import: Enporzhiañ import: Enporzhiañ
import_and_export: Enporzhiañ hag ezporzhiañ import_and_export: Enporzhiañ hag ezporzhiañ
preferences: Gwellvezioù preferences: Gwellvezioù
@ -475,6 +499,8 @@ br:
one: "%{count} skeudenn" one: "%{count} skeudenn"
other: "%{count} skeudenn" other: "%{count} skeudenn"
two: "%{count} skeudenn" two: "%{count} skeudenn"
pin_errors:
ownership: N'hallit ket spilhennañ embannadurioù ar re all
poll: poll:
vote: Mouezhiañ vote: Mouezhiañ
show_more: Diskouez muioc'h show_more: Diskouez muioc'h
@ -483,6 +509,7 @@ br:
public: Publik public: Publik
statuses_cleanup: statuses_cleanup:
keep_direct: Mirout ar c'hannadoù eeun keep_direct: Mirout ar c'hannadoù eeun
keep_media: Derc'hel an embannadurioù gant restroù stag
min_age: min_age:
'1209600': 2 sizhunvezh '1209600': 2 sizhunvezh
'2629746': 1 mizvezh '2629746': 1 mizvezh
@ -514,6 +541,7 @@ br:
subject: Donemat e Mastodoñ subject: Donemat e Mastodoñ
title: Degemer mat e bourzh, %{name}! title: Degemer mat e bourzh, %{name}!
users: users:
follow_limit_reached: N'hallit ket heulian muioc'h eget %{limit} a zen
signed_in_as: 'Aet-tre evel:' signed_in_as: 'Aet-tre evel:'
verification: verification:
verification: Amprouadur verification: Amprouadur

View File

@ -425,6 +425,7 @@ ca:
view: Veure el bloqueig del domini view: Veure el bloqueig del domini
email_domain_blocks: email_domain_blocks:
add_new: Afegir nou add_new: Afegir nou
allow_registrations_with_approval: Registre permès amb validació
attempts_over_week: attempts_over_week:
one: "%{count} intent en la darrera setmana" one: "%{count} intent en la darrera setmana"
other: "%{count} intents de registre en la darrera setmana" other: "%{count} intents de registre en la darrera setmana"

View File

@ -453,6 +453,7 @@ cy:
view: Gweld bloc parth view: Gweld bloc parth
email_domain_blocks: email_domain_blocks:
add_new: Ychwanegu add_new: Ychwanegu
allow_registrations_with_approval: Caniatáu cofrestriadau wedi'u cymeradwyo
attempts_over_week: attempts_over_week:
few: "%{count} ymgais i gofrestru dros yr wythnos ddiwethaf" few: "%{count} ymgais i gofrestru dros yr wythnos ddiwethaf"
many: "%{count} ymgais i gofrestru dros yr wythnos ddiwethaf" many: "%{count} ymgais i gofrestru dros yr wythnos ddiwethaf"

View File

@ -425,6 +425,7 @@ da:
view: Vis domæneblokering view: Vis domæneblokering
email_domain_blocks: email_domain_blocks:
add_new: Tilføj ny add_new: Tilføj ny
allow_registrations_with_approval: Tillad registreringer med godkendelse
attempts_over_week: attempts_over_week:
one: "%{count} tilmeldingsforsøg over den seneste uge" one: "%{count} tilmeldingsforsøg over den seneste uge"
other: "%{count} tilmeldingsforsøg over den seneste uge" other: "%{count} tilmeldingsforsøg over den seneste uge"

View File

@ -425,6 +425,7 @@ de:
view: Domain-Sperre ansehen view: Domain-Sperre ansehen
email_domain_blocks: email_domain_blocks:
add_new: Neue hinzufügen add_new: Neue hinzufügen
allow_registrations_with_approval: Registrierungen mit Genehmigung erlauben
attempts_over_week: attempts_over_week:
one: "%{count} Registrierungsversuch in der vergangenen Woche" one: "%{count} Registrierungsversuch in der vergangenen Woche"
other: "%{count} Registrierungsversuche in der vergangenen Woche" other: "%{count} Registrierungsversuche in der vergangenen Woche"

View File

@ -34,7 +34,7 @@ el:
explanation: Το συνθηματικό του λογαριασμού σου άλλαξε. explanation: Το συνθηματικό του λογαριασμού σου άλλαξε.
extra: Αν δεν άλλαξες εσύ το συνθηματικό σου, ίσως κάποιος να έχει αποκτήσει πρόσβαση στο λογαριασμό σου. Παρακαλούμε άλλαξε το συνθηματικό σου άμεσα ή επικοινώνησε με τον διαχειριστή του κόμβου σου αν έχεις κλειδωθεί απ' έξω. extra: Αν δεν άλλαξες εσύ το συνθηματικό σου, ίσως κάποιος να έχει αποκτήσει πρόσβαση στο λογαριασμό σου. Παρακαλούμε άλλαξε το συνθηματικό σου άμεσα ή επικοινώνησε με τον διαχειριστή του κόμβου σου αν έχεις κλειδωθεί απ' έξω.
subject: 'Mastodon: Αλλαγή συνθηματικού' subject: 'Mastodon: Αλλαγή συνθηματικού'
title: Αλλαγή συνθηματικού title: Ο κωδικός άλλαξε
reconfirmation_instructions: reconfirmation_instructions:
explanation: Επιβεβαίωσε τη νέα διεύθυνση για να αλλάξεις το email σου. explanation: Επιβεβαίωσε τη νέα διεύθυνση για να αλλάξεις το email σου.
extra: Αν δεν ζήτησες εσύ αυτή την αλλαγή, παρακαλούμε αγνόησε αυτό το email. Η διεύθυνση email για τον λογαριασμό σου στο Mastodon δεν θα αλλάξει μέχρι να επισκεφτείς τον παραπάνω σύνδεσμο. extra: Αν δεν ζήτησες εσύ αυτή την αλλαγή, παρακαλούμε αγνόησε αυτό το email. Η διεύθυνση email για τον λογαριασμό σου στο Mastodon δεν θα αλλάξει μέχρι να επισκεφτείς τον παραπάνω σύνδεσμο.

View File

@ -1 +1,31 @@
---
ia: ia:
devise:
failure:
locked: Tu conto es blocate.
mailer:
confirmation_instructions:
action: Verificar adresse de e-mail
action_with_app: Confirmar e retornar a %{app}
title: Verificar adresse de e-mail
email_changed:
title: Nove adresse de e-mail
password_change:
title: Contrasigno cambiate
reconfirmation_instructions:
title: Verificar adresse de e-mail
reset_password_instructions:
action: Cambiar contrasigno
title: Reinitialisar contrasigno
two_factor_disabled:
title: 2FA disactivate
two_factor_enabled:
title: 2FA activate
registrations:
updated: Tu conto ha essite actualisate con successo.
unlocks:
unlocked: Tu conto ha essite disblocate con successo. Initia session a continuar.
errors:
messages:
already_confirmed: jam esseva confirmate, tenta initiar session
not_found: non trovate

Some files were not shown because too many files have changed in this diff Show More