Merge branch 'main' into translate-toots

This commit is contained in:
Thomas Steiner 2025-08-08 01:50:59 +02:00 committed by GitHub
commit e3a69530a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
455 changed files with 8664 additions and 3098 deletions

View File

@ -5,6 +5,7 @@
.gitattributes .gitattributes
.gitignore .gitignore
.github .github
.vscode
public/system public/system
public/assets public/assets
public/packs public/packs
@ -20,6 +21,7 @@ postgres14
redis redis
elasticsearch elasticsearch
chart chart
storybook-static
.yarn/ .yarn/
!.yarn/patches !.yarn/patches
!.yarn/plugins !.yarn/plugins

View File

@ -50,7 +50,7 @@ jobs:
# Create or update the pull request # Create or update the pull request
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@v7.0.6 uses: peter-evans/create-pull-request@v7.0.8
with: with:
commit-message: 'New Crowdin translations' commit-message: 'New Crowdin translations'
title: 'New Crowdin Translations for ${{ github.base_ref || github.ref_name }} (automated)' title: 'New Crowdin Translations for ${{ github.base_ref || github.ref_name }} (automated)'

2
.nvmrc
View File

@ -1 +1 @@
22.17 22.18

View File

@ -1,15 +1,11 @@
# This configuration was generated by # This configuration was generated by
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp` # `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp`
# using RuboCop version 1.77.0. # using RuboCop version 1.79.2.
# 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
# versions of RuboCop, may require this file to be generated again. # versions of RuboCop, may require this file to be generated again.
Lint/NonLocalExitFromIterator:
Exclude:
- 'app/helpers/json_ld_helper.rb'
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
Metrics/AbcSize: Metrics/AbcSize:
Max: 82 Max: 82

View File

@ -2,6 +2,48 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [4.4.3] - 2025-08-05
### Security
- Update dependencies
- Fix incorrect rate-limit handling [GHSA-84ch-6436-c7mg](https://github.com/mastodon/mastodon/security/advisories/GHSA-84ch-6436-c7mg)
### Fixed
- Fix race condition caused by ActiveRecord query cache in `Create` critical path (#35662 by @ClearlyClaire)
- Fix race condition caused by quote post processing (#35657 by @ClearlyClaire)
- Fix WebUI crashing for accounts with `null` URL (#35651 by @ClearlyClaire)
- Fix friends-of-friends recommendations suggesting already-requested accounts (#35604 by @ClearlyClaire)
- Fix synchronous recursive fetching of deeply-nested quoted posts (#35600 by @ClearlyClaire)
- Fix “Expand this post” link including user `@undefined` (#35478 by @ClearlyClaire)
### Changed
- Change `StatusReachFinder` to consider quotes as well as reblogs (#35601 by @ClearlyClaire)
- Add restrictions on which quote posts can trend (#35507 by @ClearlyClaire)
- Change quote verification to not bypass authorization flow for mentions (#35528 by @ClearlyClaire)
## [4.4.2] - 2025-07-23
### Security
- Update dependencies
### Fixed
- Fix menu not clickable in Firefox (#35390 and #35414 by @diondiondion)
- Add `lang` attribute to current composer language in alt text modal (#35412 by @diondiondion)
- Fix quote posts styling on notifications page (#35411 by @diondiondion)
- Improve a11y of custom select menus in notifications settings (#35403 by @diondiondion)
- Fix selected item in poll select menus is unreadable in Firefox (#35402 by @diondiondion)
- Update age limit wording (#35387 by @diondiondion)
- Fix support for quote verification in implicit status updates (#35384 by @ClearlyClaire)
- Improve `Dropdown` component accessibility (#35373 by @diondiondion)
- Fix processing some incoming quotes failing because of missing JSON-LD context (#35354 and #35380 by @ClearlyClaire)
- Make bio hashtags open the local page instead of the remote instance (#35349 by @ChaosExAnima)
- Fix styling of external log-in button (#35320 by @ClearlyClaire)
## [4.4.1] - 2025-07-09 ## [4.4.1] - 2025-07-09
### Fixed ### Fixed

View File

@ -84,7 +84,7 @@ gem 'sanitize', '~> 7.0'
gem 'scenic', '~> 1.7' gem 'scenic', '~> 1.7'
gem 'sidekiq', '< 8' gem 'sidekiq', '< 8'
gem 'sidekiq-bulk', '~> 0.2.0' gem 'sidekiq-bulk', '~> 0.2.0'
gem 'sidekiq-scheduler', '~> 5.0' gem 'sidekiq-scheduler', '~> 6.0'
gem 'sidekiq-unique-jobs', '> 8' gem 'sidekiq-unique-jobs', '> 8'
gem 'simple_form', '~> 5.2' gem 'simple_form', '~> 5.2'
gem 'simple-navigation', '~> 4.4' gem 'simple-navigation', '~> 4.4'

View File

@ -90,13 +90,13 @@ GEM
public_suffix (>= 2.0.2, < 7.0) public_suffix (>= 2.0.2, < 7.0)
aes_key_wrap (1.1.0) aes_key_wrap (1.1.0)
android_key_attestation (0.3.0) android_key_attestation (0.3.0)
annotaterb (4.17.0) annotaterb (4.18.0)
activerecord (>= 6.0.0) activerecord (>= 6.0.0)
activesupport (>= 6.0.0) activesupport (>= 6.0.0)
ast (2.4.3) ast (2.4.3)
attr_required (1.0.2) attr_required (1.0.2)
aws-eventstream (1.4.0) aws-eventstream (1.4.0)
aws-partitions (1.1131.0) aws-partitions (1.1135.0)
aws-sdk-core (3.215.1) aws-sdk-core (3.215.1)
aws-eventstream (~> 1, >= 1.3.0) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0) aws-partitions (~> 1, >= 1.992.0)
@ -144,7 +144,7 @@ GEM
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0) regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2) xpath (~> 3.2)
capybara-playwright-driver (0.5.6) capybara-playwright-driver (0.5.7)
addressable addressable
capybara capybara
playwright-ruby-client (>= 1.16.0) playwright-ruby-client (>= 1.16.0)
@ -175,9 +175,9 @@ GEM
css_parser (1.21.1) css_parser (1.21.1)
addressable addressable
csv (3.3.5) csv (3.3.5)
database_cleaner-active_record (2.2.1) database_cleaner-active_record (2.2.2)
activerecord (>= 5.a) activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0) database_cleaner-core (~> 2.0)
database_cleaner-core (2.0.1) database_cleaner-core (2.0.1)
date (3.4.1) date (3.4.1)
debug (1.11.0) debug (1.11.0)
@ -233,7 +233,7 @@ GEM
fabrication (3.0.0) fabrication (3.0.0)
faker (3.5.2) faker (3.5.2)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
faraday (2.13.2) faraday (2.13.4)
faraday-net_http (>= 2.0, < 3.5) faraday-net_http (>= 2.0, < 3.5)
json json
logger logger
@ -287,7 +287,7 @@ GEM
activesupport (>= 5.1) activesupport (>= 5.1)
haml (>= 4.0.6) haml (>= 4.0.6)
railties (>= 5.1) railties (>= 5.1)
haml_lint (0.65.0) haml_lint (0.66.0)
haml (>= 5.0) haml (>= 5.0)
parallel (~> 1.10) parallel (~> 1.10)
rainbow rainbow
@ -315,7 +315,7 @@ GEM
http_accept_language (2.1.1) http_accept_language (2.1.1)
httpclient (2.9.0) httpclient (2.9.0)
mutex_m mutex_m
httplog (1.7.1) httplog (1.7.2)
rack (>= 2.0) rack (>= 2.0)
rainbow (>= 2.0.0) rainbow (>= 2.0.0)
i18n (1.14.7) i18n (1.14.7)
@ -345,7 +345,7 @@ GEM
azure-blob (~> 0.5.2) azure-blob (~> 0.5.2)
hashie (~> 5.0) hashie (~> 5.0)
jmespath (1.6.2) jmespath (1.6.2)
json (2.13.0) json (2.13.2)
json-canonicalization (1.0.0) json-canonicalization (1.0.0)
json-jwt (1.16.7) json-jwt (1.16.7)
activesupport (>= 4.2) activesupport (>= 4.2)
@ -438,7 +438,7 @@ GEM
mime-types (3.7.0) mime-types (3.7.0)
logger logger
mime-types-data (~> 3.2025, >= 3.2025.0507) mime-types-data (~> 3.2025, >= 3.2025.0507)
mime-types-data (3.2025.0715) mime-types-data (3.2025.0729)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.9) mini_portile2 (2.8.9)
minitest (5.25.5) minitest (5.25.5)
@ -468,7 +468,7 @@ GEM
hashie (>= 3.4.6) hashie (>= 3.4.6)
rack (>= 2.2.3) rack (>= 2.2.3)
rack-protection rack-protection
omniauth-cas (3.0.1) omniauth-cas (3.0.2)
addressable (~> 2.8) addressable (~> 2.8)
nokogiri (~> 1.12) nokogiri (~> 1.12)
omniauth (~> 2.1) omniauth (~> 2.1)
@ -601,16 +601,16 @@ GEM
ox (2.14.23) ox (2.14.23)
bigdecimal (>= 3.0) bigdecimal (>= 3.0)
parallel (1.27.0) parallel (1.27.0)
parser (3.3.8.0) parser (3.3.9.0)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
parslet (2.0.0) parslet (2.0.0)
pastel (0.8.0) pastel (0.8.0)
tty-color (~> 0.5) tty-color (~> 0.5)
pg (1.5.9) pg (1.6.1)
pghero (3.7.0) pghero (3.7.0)
activerecord (>= 7.1) activerecord (>= 7.1)
playwright-ruby-client (1.54.0) playwright-ruby-client (1.54.1)
concurrent-ruby (>= 1.1.6) concurrent-ruby (>= 1.1.6)
mime-types (>= 3.0) mime-types (>= 3.0)
pp (0.6.2) pp (0.6.2)
@ -635,7 +635,7 @@ GEM
date date
stringio stringio
public_suffix (6.0.2) public_suffix (6.0.2)
puma (6.6.0) puma (6.6.1)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.5.0) pundit (2.5.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
@ -721,7 +721,7 @@ GEM
connection_pool connection_pool
redlock (1.3.2) redlock (1.3.2)
redis (>= 3.0.0, < 6.0) redis (>= 3.0.0, < 6.0)
regexp_parser (2.10.0) regexp_parser (2.11.0)
reline (0.6.2) reline (0.6.2)
io-console (~> 0.5) io-console (~> 0.5)
request_store (1.7.0) request_store (1.7.0)
@ -731,7 +731,7 @@ GEM
railties (>= 5.2) railties (>= 5.2)
rexml (3.4.1) rexml (3.4.1)
rotp (6.3.0) rotp (6.3.0)
rouge (4.5.2) rouge (4.6.0)
rpam2 (4.0.2) rpam2 (4.0.2)
rqrcode (3.1.0) rqrcode (3.1.0)
chunky_png (~> 1.0) chunky_png (~> 1.0)
@ -765,7 +765,7 @@ GEM
rspec-mocks (~> 3.0) rspec-mocks (~> 3.0)
sidekiq (>= 5, < 9) sidekiq (>= 5, < 9)
rspec-support (3.13.4) rspec-support (3.13.4)
rubocop (1.78.0) rubocop (1.79.2)
json (~> 2.3) json (~> 2.3)
language_server-protocol (~> 3.17.0.2) language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0) lint_roller (~> 1.1.0)
@ -773,7 +773,7 @@ GEM
parser (>= 3.3.0.2) parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0) regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.45.1, < 2.0) rubocop-ast (>= 1.46.0, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0) unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.46.0) rubocop-ast (1.46.0)
@ -805,7 +805,7 @@ GEM
ruby-prof (1.7.2) ruby-prof (1.7.2)
base64 base64
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
ruby-saml (1.18.0) ruby-saml (1.18.1)
nokogiri (>= 1.13.10) nokogiri (>= 1.13.10)
rexml rexml
ruby-vips (2.2.4) ruby-vips (2.2.4)
@ -833,10 +833,9 @@ GEM
redis-client (>= 0.22.2) redis-client (>= 0.22.2)
sidekiq-bulk (0.2.0) sidekiq-bulk (0.2.0)
sidekiq sidekiq
sidekiq-scheduler (5.0.6) sidekiq-scheduler (6.0.1)
rufus-scheduler (~> 3.2) rufus-scheduler (~> 3.2)
sidekiq (>= 6, < 8) sidekiq (>= 7.3, < 9)
tilt (>= 1.4.0, < 3)
sidekiq-unique-jobs (8.0.11) sidekiq-unique-jobs (8.0.11)
concurrent-ruby (~> 1.0, >= 1.0.5) concurrent-ruby (~> 1.0, >= 1.0.5)
sidekiq (>= 7.0.0, < 9.0.0) sidekiq (>= 7.0.0, < 9.0.0)
@ -859,7 +858,7 @@ GEM
stoplight (4.1.1) stoplight (4.1.1)
redlock (~> 1.0) redlock (~> 1.0)
stringio (3.1.7) stringio (3.1.7)
strong_migrations (2.4.0) strong_migrations (2.5.0)
activerecord (>= 7.1) activerecord (>= 7.1)
swd (2.0.3) swd (2.0.3)
activesupport (>= 3) activesupport (>= 3)
@ -867,7 +866,7 @@ GEM
faraday (~> 2.0) faraday (~> 2.0)
faraday-follow_redirects faraday-follow_redirects
sysexits (1.2.0) sysexits (1.2.0)
temple (0.10.3) temple (0.10.4)
terminal-table (4.0.0) terminal-table (4.0.0)
unicode-display_width (>= 1.1.1, < 4) unicode-display_width (>= 1.1.1, < 4)
terrapin (1.1.1) terrapin (1.1.1)
@ -1082,7 +1081,7 @@ DEPENDENCIES
shoulda-matchers shoulda-matchers
sidekiq (< 8) sidekiq (< 8)
sidekiq-bulk (~> 0.2.0) sidekiq-bulk (~> 0.2.0)
sidekiq-scheduler (~> 5.0) sidekiq-scheduler (~> 6.0)
sidekiq-unique-jobs (> 8) sidekiq-unique-jobs (> 8)
simple-navigation (~> 4.4) simple-navigation (~> 4.4)
simple_form (~> 5.2) simple_form (~> 5.2)
@ -1106,4 +1105,4 @@ RUBY VERSION
ruby 3.4.1p0 ruby 3.4.1p0
BUNDLED WITH BUNDLED WITH
2.7.0 2.7.1

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
class ActivityPub::QuoteAuthorizationsController < ActivityPub::BaseController
include Authorization
vary_by -> { 'Signature' if authorized_fetch_mode? }
before_action :require_account_signature!, if: :authorized_fetch_mode?
before_action :set_quote_authorization
def show
expires_in 0, public: @quote.status.distributable? && public_fetch_mode?
render json: @quote, serializer: ActivityPub::QuoteAuthorizationSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end
private
def pundit_user
signed_request_account
end
def set_quote_authorization
@quote = Quote.accepted.where(quoted_account: @account).find(params[:id])
authorize @quote.status, :show?
rescue Mastodon::NotPermittedError
not_found
end
end

View File

@ -6,7 +6,7 @@ module Admin
def index def index
authorize :audit_log, :index? authorize :audit_log, :index?
@auditable_accounts = Account.auditable.select(:id, :username) @auditable_accounts = Account.auditable.select(:id, :username).order(username: :asc)
end end
private private

View File

@ -19,15 +19,13 @@ module Admin
log_action :resend, @user log_action :resend, @user
flash[:notice] = I18n.t('admin.accounts.resend_confirmation.success') redirect_to admin_accounts_path, notice: t('admin.accounts.resend_confirmation.success')
redirect_to admin_accounts_path
end end
private private
def redirect_confirmed_user def redirect_confirmed_user
flash[:error] = I18n.t('admin.accounts.resend_confirmation.already_confirmed') redirect_to admin_accounts_path, flash: { error: t('admin.accounts.resend_confirmation.already_confirmed') }
redirect_to admin_accounts_path
end end
def user_confirmed? def user_confirmed?

View File

@ -18,7 +18,7 @@ class Admin::Disputes::AppealsController < Admin::BaseController
end end
def reject def reject
authorize @appeal, :approve? authorize @appeal, :reject?
log_action :reject, @appeal log_action :reject, @appeal
@appeal.reject!(current_account) @appeal.reject!(current_account)
UserMailer.appeal_rejected(@appeal.account.user, @appeal).deliver_later UserMailer.appeal_rejected(@appeal.account.user, @appeal).deliver_later

View File

@ -36,7 +36,7 @@ module Admin
end end
def edit def edit
authorize :domain_block, :create? authorize :domain_block, :update?
end end
def create def create
@ -129,7 +129,7 @@ module Admin
end end
def requires_confirmation? def requires_confirmation?
@domain_block.valid? && (@domain_block.new_record? || @domain_block.severity_changed?) && @domain_block.severity.to_s == 'suspend' && !params[:confirm] @domain_block.valid? && (@domain_block.new_record? || @domain_block.severity_changed?) && @domain_block.suspend? && !params[:confirm]
end end
end end
end end

View File

@ -14,8 +14,7 @@ module Admin
@admin_settings = Form::AdminSettings.new(settings_params) @admin_settings = Form::AdminSettings.new(settings_params)
if @admin_settings.save if @admin_settings.save
flash[:notice] = I18n.t('generic.changes_saved_msg') redirect_to after_update_redirect_path, notice: t('generic.changes_saved_msg')
redirect_to after_update_redirect_path
else else
render :show render :show
end end

View File

@ -0,0 +1,77 @@
# frozen_string_literal: true
class Admin::UsernameBlocksController < Admin::BaseController
before_action :set_username_block, only: [:edit, :update]
def index
authorize :username_block, :index?
@username_blocks = UsernameBlock.order(username: :asc).page(params[:page])
@form = Form::UsernameBlockBatch.new
end
def batch
authorize :username_block, :index?
@form = Form::UsernameBlockBatch.new(form_username_block_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.username_blocks.no_username_block_selected')
rescue Mastodon::NotPermittedError
flash[:alert] = I18n.t('admin.username_blocks.not_permitted')
ensure
redirect_to admin_username_blocks_path
end
def new
authorize :username_block, :create?
@username_block = UsernameBlock.new(exact: true)
end
def edit
authorize @username_block, :update?
end
def create
authorize :username_block, :create?
@username_block = UsernameBlock.new(resource_params)
if @username_block.save
log_action :create, @username_block
redirect_to admin_username_blocks_path, notice: I18n.t('admin.username_blocks.created_msg')
else
render :new
end
end
def update
authorize @username_block, :update?
if @username_block.update(resource_params)
log_action :update, @username_block
redirect_to admin_username_blocks_path, notice: I18n.t('admin.username_blocks.updated_msg')
else
render :new
end
end
private
def set_username_block
@username_block = UsernameBlock.find(params[:id])
end
def form_username_block_batch_params
params
.expect(form_username_block_batch: [username_block_ids: []])
end
def resource_params
params
.expect(username_block: [:username, :comparison, :allow_with_approval])
end
def action_from_button
'delete' if params[:delete]
end
end

View File

@ -2,6 +2,7 @@
class Api::V1::Admin::TagsController < Api::BaseController class Api::V1::Admin::TagsController < Api::BaseController
include Authorization include Authorization
before_action -> { authorize_if_got_token! :'admin:read' }, only: [:index, :show] before_action -> { authorize_if_got_token! :'admin:read' }, only: [:index, :show]
before_action -> { authorize_if_got_token! :'admin:write' }, only: :update before_action -> { authorize_if_got_token! :'admin:write' }, only: :update

View File

@ -16,16 +16,7 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
def create def create
with_redis_lock("push_subscription:#{current_user.id}") do with_redis_lock("push_subscription:#{current_user.id}") do
destroy_web_push_subscriptions! destroy_web_push_subscriptions!
@push_subscription = Web::PushSubscription.create!(web_push_subscription_params)
@push_subscription = Web::PushSubscription.create!(
endpoint: subscription_params[:endpoint],
key_p256dh: subscription_params[:keys][:p256dh],
key_auth: subscription_params[:keys][:auth],
standard: subscription_params[:standard] || false,
data: data_params,
user_id: current_user.id,
access_token_id: doorkeeper_token.id
)
end end
render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
@ -55,6 +46,18 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
not_found if @push_subscription.nil? not_found if @push_subscription.nil?
end end
def web_push_subscription_params
{
access_token_id: doorkeeper_token.id,
data: data_params,
endpoint: subscription_params[:endpoint],
key_auth: subscription_params[:keys][:auth],
key_p256dh: subscription_params[:keys][:p256dh],
standard: subscription_params[:standard] || false,
user_id: current_user.id,
}
end
def subscription_params def subscription_params
params.expect(subscription: [:endpoint, :standard, keys: [:auth, :p256dh]]) params.expect(subscription: [:endpoint, :standard, keys: [:auth, :p256dh]])
end end

View File

@ -0,0 +1,72 @@
# frozen_string_literal: true
class Api::V1::Statuses::QuotesController < Api::V1::Statuses::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: :index
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: :revoke
before_action :check_owner!
before_action :set_quote, only: :revoke
after_action :insert_pagination_headers, only: :index
def index
cache_if_unauthenticated!
@statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer
end
def revoke
authorize @quote, :revoke?
RevokeQuoteService.new.call(@quote)
render json: @quote.status, serializer: REST::StatusSerializer
end
private
def check_owner!
authorize @status, :list_quotes?
end
def set_quote
@quote = @status.quotes.find_by!(status_id: params[:id])
end
def load_statuses
scope = default_statuses
scope = scope.not_excluded_by_account(current_account) unless current_account.nil?
scope.merge(paginated_quotes).to_a
end
def default_statuses
Status.includes(:quote).references(:quote)
end
def paginated_quotes
@status.quotes.accepted.paginate_by_max_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id]
)
end
def next_path
api_v1_status_quotes_url pagination_params(max_id: pagination_max_id) if records_continue?
end
def prev_path
api_v1_status_quotes_url pagination_params(since_id: pagination_since_id) unless @statuses.empty?
end
def pagination_max_id
@statuses.last.quote.id
end
def pagination_since_id
@statuses.first.quote.id
end
def records_continue?
@statuses.size == limit_param(DEFAULT_STATUSES_LIMIT)
end
end

View File

@ -10,6 +10,7 @@ class Api::V1::StatusesController < Api::BaseController
before_action :set_statuses, only: [:index] before_action :set_statuses, only: [:index]
before_action :set_status, only: [:show, :context] before_action :set_status, only: [:show, :context]
before_action :set_thread, only: [:create] before_action :set_thread, only: [:create]
before_action :set_quoted_status, only: [:create]
before_action :check_statuses_limit, only: [:index] before_action :check_statuses_limit, only: [:index]
override_rate_limit_headers :create, family: :statuses override_rate_limit_headers :create, family: :statuses
@ -65,7 +66,11 @@ class Api::V1::StatusesController < Api::BaseController
add_async_refresh_header(async_refresh) add_async_refresh_header(async_refresh)
elsif !current_account.nil? && @status.should_fetch_replies? elsif !current_account.nil? && @status.should_fetch_replies?
add_async_refresh_header(AsyncRefresh.create(refresh_key)) add_async_refresh_header(AsyncRefresh.create(refresh_key))
ActivityPub::FetchAllRepliesWorker.perform_async(@status.id)
WorkerBatch.new.within do |batch|
batch.connect(refresh_key, threshold: 1.0)
ActivityPub::FetchAllRepliesWorker.perform_async(@status.id, { 'batch_id' => batch.id })
end
end end
render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id) render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
@ -76,6 +81,8 @@ class Api::V1::StatusesController < Api::BaseController
current_user.account, current_user.account,
text: status_params[:status], text: status_params[:status],
thread: @thread, thread: @thread,
quoted_status: @quoted_status,
quote_approval_policy: quote_approval_policy,
media_ids: status_params[:media_ids], media_ids: status_params[:media_ids],
sensitive: status_params[:sensitive], sensitive: status_params[:sensitive],
spoiler_text: status_params[:spoiler_text], spoiler_text: status_params[:spoiler_text],
@ -107,7 +114,8 @@ class Api::V1::StatusesController < Api::BaseController
sensitive: status_params[:sensitive], sensitive: status_params[:sensitive],
language: status_params[:language], language: status_params[:language],
spoiler_text: status_params[:spoiler_text], spoiler_text: status_params[:spoiler_text],
poll: status_params[:poll] poll: status_params[:poll],
quote_approval_policy: quote_approval_policy
) )
render json: @status, serializer: REST::StatusSerializer render json: @status, serializer: REST::StatusSerializer
@ -147,6 +155,16 @@ class Api::V1::StatusesController < Api::BaseController
render json: { error: I18n.t('statuses.errors.in_reply_not_found') }, status: 404 render json: { error: I18n.t('statuses.errors.in_reply_not_found') }, status: 404
end end
def set_quoted_status
return unless Mastodon::Feature.outgoing_quotes_enabled?
@quoted_status = Status.find(status_params[:quoted_status_id]) if status_params[:quoted_status_id].present?
authorize(@quoted_status, :quote?) if @quoted_status.present?
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
# TODO: distinguish between non-existing and non-quotable posts
render json: { error: I18n.t('statuses.errors.quoted_status_not_found') }, status: 404
end
def check_statuses_limit def check_statuses_limit
raise(Mastodon::ValidationError) if status_ids.size > DEFAULT_STATUSES_LIMIT raise(Mastodon::ValidationError) if status_ids.size > DEFAULT_STATUSES_LIMIT
end end
@ -163,6 +181,8 @@ class Api::V1::StatusesController < Api::BaseController
params.permit( params.permit(
:status, :status,
:in_reply_to_id, :in_reply_to_id,
:quoted_status_id,
:quote_approval_policy,
:sensitive, :sensitive,
:spoiler_text, :spoiler_text,
:visibility, :visibility,
@ -185,6 +205,23 @@ class Api::V1::StatusesController < Api::BaseController
) )
end end
def quote_approval_policy
# TODO: handle `nil` separately
return nil unless Mastodon::Feature.outgoing_quotes_enabled? && status_params[:quote_approval_policy].present?
case status_params[:quote_approval_policy]
when 'public'
Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16
when 'followers'
Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16
when 'nobody'
0
else
# TODO: raise more useful message
raise ActiveRecord::RecordInvalid
end
end
def serializer_for_status def serializer_for_status
@status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
end end

View File

@ -20,7 +20,7 @@ class Api::V2::SearchController < Api::BaseController
@search = Search.new(search_results) @search = Search.new(search_results)
render json: @search, serializer: REST::SearchSerializer render json: @search, serializer: REST::SearchSerializer
rescue Mastodon::SyntaxError rescue Mastodon::SyntaxError
unprocessable_entity unprocessable_content
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
not_found not_found
end end

View File

@ -49,7 +49,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
{ {
policy: 'all', policy: 'all',
alerts: Notification::TYPES.index_with { alerts_enabled }, alerts: Notification::TYPES.index_with { alerts_enabled },
} }.deep_stringify_keys
end end
def alerts_enabled def alerts_enabled

View File

@ -28,7 +28,7 @@ class ApplicationController < ActionController::Base
rescue_from Mastodon::NotPermittedError, with: :forbidden rescue_from Mastodon::NotPermittedError, with: :forbidden
rescue_from ActionController::RoutingError, ActiveRecord::RecordNotFound, with: :not_found rescue_from ActionController::RoutingError, ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActionController::UnknownFormat, with: :not_acceptable rescue_from ActionController::UnknownFormat, with: :not_acceptable
rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_content
rescue_from Mastodon::RateLimitExceededError, with: :too_many_requests rescue_from Mastodon::RateLimitExceededError, with: :too_many_requests
rescue_from(*Mastodon::HTTP_CONNECTION_ERRORS, with: :internal_server_error) rescue_from(*Mastodon::HTTP_CONNECTION_ERRORS, with: :internal_server_error)
@ -123,7 +123,7 @@ class ApplicationController < ActionController::Base
respond_with_error(410) respond_with_error(410)
end end
def unprocessable_entity def unprocessable_content
respond_with_error(422) respond_with_error(422)
end end

View File

@ -19,8 +19,7 @@ class Auth::PasswordsController < Devise::PasswordsController
private private
def redirect_invalid_reset_token def redirect_invalid_reset_token
flash[:error] = I18n.t('auth.invalid_reset_password_token') redirect_to new_password_path(resource_name), flash: { error: t('auth.invalid_reset_password_token') }
redirect_to new_password_path(resource_name)
end end
def reset_password_token_is_valid? def reset_password_token_is_valid?

View File

@ -22,7 +22,7 @@ class Settings::Migration::RedirectsController < Settings::BaseController
end end
def destroy def destroy
if current_account.moved_to_account_id.present? if current_account.moved?
current_account.update!(moved_to_account: nil) current_account.update!(moved_to_account: nil)
ActivityPub::UpdateDistributionWorker.perform_async(current_account.id) ActivityPub::UpdateDistributionWorker.perform_async(current_account.id)
end end

View File

@ -8,8 +8,7 @@ class Settings::SessionsController < Settings::BaseController
def destroy def destroy
@session.destroy! @session.destroy!
flash[:notice] = I18n.t('sessions.revoke_success') redirect_to edit_user_registration_path, notice: t('sessions.revoke_success')
redirect_to edit_user_registration_path
end end
private private

View File

@ -52,7 +52,7 @@ module Settings
end end
else else
flash[:error] = I18n.t('webauthn_credentials.create.error') flash[:error] = I18n.t('webauthn_credentials.create.error')
status = :unprocessable_entity status = :unprocessable_content
end end
else else
flash[:error] = t('webauthn_credentials.create.error') flash[:error] = t('webauthn_credentials.create.error')
@ -86,13 +86,11 @@ module Settings
private private
def redirect_invalid_otp def redirect_invalid_otp
flash[:error] = t('webauthn_credentials.otp_required') redirect_to settings_two_factor_authentication_methods_path, flash: { error: t('webauthn_credentials.otp_required') }
redirect_to settings_two_factor_authentication_methods_path
end end
def redirect_invalid_webauthn def redirect_invalid_webauthn
flash[:error] = t('webauthn_credentials.not_enabled') redirect_to settings_two_factor_authentication_methods_path, flash: { error: t('webauthn_credentials.not_enabled') }
redirect_to settings_two_factor_authentication_methods_path
end end
end end
end end

View File

@ -11,6 +11,7 @@ class StatusesController < ApplicationController
before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? } before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_status before_action :set_status
before_action :redirect_to_original, only: :show before_action :redirect_to_original, only: :show
before_action :verify_embed_allowed, only: :embed
after_action :set_link_headers after_action :set_link_headers
@ -40,8 +41,6 @@ class StatusesController < ApplicationController
end end
def embed def embed
return not_found if @status.hidden? || @status.reblog?
expires_in 180, public: true expires_in 180, public: true
response.headers.delete('X-Frame-Options') response.headers.delete('X-Frame-Options')
@ -50,6 +49,10 @@ class StatusesController < ApplicationController
private private
def verify_embed_allowed
not_found if @status.hidden? || @status.reblog?
end
def set_link_headers def set_link_headers
response.headers['Link'] = LinkHeader.new( response.headers['Link'] = LinkHeader.new(
[[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]]] [[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]]]

View File

@ -13,6 +13,8 @@ module Admin::ActionLogsHelper
end end
when 'UserRole' when 'UserRole'
link_to log.human_identifier, admin_roles_path(log.target_id) link_to log.human_identifier, admin_roles_path(log.target_id)
when 'UsernameBlock'
link_to log.human_identifier, edit_admin_username_block_path(log.target_id)
when 'Report' when 'Report'
link_to "##{log.human_identifier.presence || log.target_id}", admin_report_path(log.target_id) link_to "##{log.human_identifier.presence || log.target_id}", admin_report_path(log.target_id)
when 'Instance', 'DomainBlock', 'DomainAllow', 'UnavailableDomain' when 'Instance', 'DomainBlock', 'DomainAllow', 'UnavailableDomain'

View File

@ -39,6 +39,12 @@ module ContextHelper
'automaticApproval' => { '@id' => 'gts:automaticApproval', '@type' => '@id' }, 'automaticApproval' => { '@id' => 'gts:automaticApproval', '@type' => '@id' },
'manualApproval' => { '@id' => 'gts:manualApproval', '@type' => '@id' }, 'manualApproval' => { '@id' => 'gts:manualApproval', '@type' => '@id' },
}, },
quote_authorizations: {
'gts' => 'https://gotosocial.org/ns#',
'quoteAuthorization' => { '@id' => 'https://w3id.org/fep/044f#quoteAuthorization', '@type' => '@id' },
'interactingObject' => { '@id' => 'gts:interactingObject' },
'interactionTarget' => { '@id' => 'gts:interactionTarget' },
},
}.freeze }.freeze
def full_context def full_context

View File

@ -1,18 +0,0 @@
# frozen_string_literal: true
module EmailHelper
def self.included(base)
base.extend(self)
end
def email_to_canonical_email(str)
username, domain = str.downcase.split('@', 2)
username, = username.delete('.').split('+', 2)
"#{username}@#{domain}"
end
def email_to_canonical_email_hash(str)
Digest::SHA2.new(256).hexdigest(email_to_canonical_email(str))
end
end

View File

@ -65,12 +65,12 @@ module FormattingHelper
end end
def rss_content_preroll(status) def rss_content_preroll(status)
if status.spoiler_text? return unless status.spoiler_text?
safe_join [
tag.p { spoiler_with_warning(status) }, safe_join [
tag.hr, tag.p { spoiler_with_warning(status) },
] tag.hr,
end ]
end end
def spoiler_with_warning(status) def spoiler_with_warning(status)
@ -81,10 +81,10 @@ module FormattingHelper
end end
def rss_content_postroll(status) def rss_content_postroll(status)
if status.preloadable_poll return unless status.preloadable_poll
tag.p do
poll_option_tags(status) tag.p do
end poll_option_tags(status)
end end
end end

View File

@ -39,18 +39,8 @@ module HomeHelper
end end
end end
def obscured_counter(count) def field_verified_class(verified)
if count <= 0 if verified
'0'
elsif count == 1
'1'
else
'1+'
end
end
def custom_field_classes(field)
if field.verified?
'verified' 'verified'
else else
'emojify' 'emojify'

View File

@ -134,7 +134,7 @@ module JsonLdHelper
patch_for_forwarding!(value, compacted_value) patch_for_forwarding!(value, compacted_value)
elsif value.is_a?(Array) elsif value.is_a?(Array)
compacted_value = [compacted_value] unless compacted_value.is_a?(Array) compacted_value = [compacted_value] unless compacted_value.is_a?(Array)
return if value.size != compacted_value.size return nil if value.size != compacted_value.size
compacted[key] = value.zip(compacted_value).map do |v, vc| compacted[key] = value.zip(compacted_value).map do |v, vc|
if v.is_a?(Hash) && vc.is_a?(Hash) if v.is_a?(Hash) && vc.is_a?(Hash)

View File

@ -24,24 +24,24 @@ module ThemeHelper
end end
def custom_stylesheet def custom_stylesheet
if active_custom_stylesheet.present? return if active_custom_stylesheet.blank?
stylesheet_link_tag(
custom_css_path(active_custom_stylesheet), stylesheet_link_tag(
host: root_url, custom_css_path(active_custom_stylesheet),
media: :all, host: root_url,
skip_pipeline: true media: :all,
) skip_pipeline: true
end )
end end
private private
def active_custom_stylesheet def active_custom_stylesheet
if cached_custom_css_digest.present? return if cached_custom_css_digest.blank?
[:custom, cached_custom_css_digest.to_s.first(8)]
.compact_blank [:custom, cached_custom_css_digest.to_s.first(8)]
.join('-') .compact_blank
end .join('-')
end end
def cached_custom_css_digest def cached_custom_css_digest

View File

@ -1 +1,3 @@
Images in this folder are based on [Tabler.io icons](https://tabler.io/icons). Images in this folder are based on [Tabler.io icons](https://tabler.io/icons).
Seems to be 1.5 width icons scaled to 64×64px and centered above a blue square with round corners (24px).

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -1,4 +1,8 @@
import { apiReblog, apiUnreblog } from 'mastodon/api/interactions'; import {
apiReblog,
apiUnreblog,
apiRevokeQuote,
} from 'mastodon/api/interactions';
import type { StatusVisibility } from 'mastodon/models/status'; import type { StatusVisibility } from 'mastodon/models/status';
import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
@ -33,3 +37,19 @@ export const unreblog = createDataLoadingThunk(
return discardLoadData; return discardLoadData;
}, },
); );
export const revokeQuote = createDataLoadingThunk(
'status/revoke_quote',
({
statusId,
quotedStatusId,
}: {
statusId: string;
quotedStatusId: string;
}) => apiRevokeQuote(quotedStatusId, statusId),
(data, { dispatch, discardLoadData }) => {
dispatch(importFetchedStatus(data));
return discardLoadData;
},
);

View File

@ -31,7 +31,9 @@ import { NOTIFICATIONS_FILTER_SET } from './notifications';
import { saveSettings } from './settings'; import { saveSettings } from './settings';
function excludeAllTypesExcept(filter: string) { function excludeAllTypesExcept(filter: string) {
return allNotificationTypes.filter((item) => item !== filter); return allNotificationTypes.filter(
(item) => item !== filter && !(item === 'quote' && filter === 'mention'),
);
} }
function getExcludedTypes(state: RootState) { function getExcludedTypes(state: RootState) {
@ -156,12 +158,15 @@ export const processNewNotificationForGroups = createAppAsyncThunk(
const showInColumn = const showInColumn =
activeFilter === 'all' activeFilter === 'all'
? notificationShows[notification.type] !== false ? notificationShows[notification.type] !== false
: activeFilter === notification.type; : activeFilter === notification.type ||
(activeFilter === 'mention' && notification.type === 'quote');
if (!showInColumn) return; if (!showInColumn) return;
if ( if (
(notification.type === 'mention' || notification.type === 'update') && (notification.type === 'mention' ||
notification.type === 'update' ||
notification.type === 'quote') &&
notification.status?.filtered notification.status?.filtered
) { ) {
const filters = notification.status.filtered.filter((result) => const filters = notification.status.filtered.filter((result) =>

View File

@ -31,7 +31,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
let filtered = false; let filtered = false;
if (['mention', 'status'].includes(notification.type) && notification.status.filtered) { if (['mention', 'status', 'quote'].includes(notification.type) && notification.status.filtered) {
const filters = notification.status.filtered.filter(result => result.filter.context.includes('notifications')); const filters = notification.status.filtered.filter(result => result.filter.context.includes('notifications'));
if (filters.some(result => result.filter.filter_action === 'hide')) { if (filters.some(result => result.filter.filter_action === 'hide')) {

View File

@ -8,3 +8,8 @@ export const apiReblog = (statusId: string, visibility: StatusVisibility) =>
export const apiUnreblog = (statusId: string) => export const apiUnreblog = (statusId: string) =>
apiRequestPost<Status>(`v1/statuses/${statusId}/unreblog`); apiRequestPost<Status>(`v1/statuses/${statusId}/unreblog`);
export const apiRevokeQuote = (quotedStatusId: string, statusId: string) =>
apiRequestPost<Status>(
`v1/statuses/${quotedStatusId}/quotes/${statusId}/revoke`,
);

View File

@ -37,7 +37,7 @@ export interface BaseApiAccountJSON {
roles?: ApiAccountJSON[]; roles?: ApiAccountJSON[];
statuses_count: number; statuses_count: number;
uri: string; uri: string;
url: string; url?: string;
username: string; username: string;
moved?: ApiAccountJSON; moved?: ApiAccountJSON;
suspended?: boolean; suspended?: boolean;

View File

@ -13,6 +13,7 @@ export const allNotificationTypes = [
'favourite', 'favourite',
'reblog', 'reblog',
'mention', 'mention',
'quote',
'poll', 'poll',
'status', 'status',
'update', 'update',
@ -28,6 +29,7 @@ export type NotificationWithStatusType =
| 'reblog' | 'reblog'
| 'status' | 'status'
| 'mention' | 'mention'
| 'quote'
| 'poll' | 'poll'
| 'update'; | 'update';

View File

@ -3,8 +3,8 @@ import { useCallback } from 'react';
import { useLinks } from 'mastodon/hooks/useLinks'; import { useLinks } from 'mastodon/hooks/useLinks';
import { EmojiHTML } from '../features/emoji/emoji_html'; import { EmojiHTML } from '../features/emoji/emoji_html';
import { isFeatureEnabled } from '../initial_state';
import { useAppSelector } from '../store'; import { useAppSelector } from '../store';
import { isModernEmojiEnabled } from '../utils/environment';
interface AccountBioProps { interface AccountBioProps {
className: string; className: string;
@ -32,9 +32,7 @@ export const AccountBio: React.FC<AccountBioProps> = ({
if (!account) { if (!account) {
return ''; return '';
} }
return isFeatureEnabled('modern_emojis') return isModernEmojiEnabled() ? account.note : account.note_emojified;
? account.note
: account.note_emojified;
}); });
const extraEmojis = useAppSelector((state) => { const extraEmojis = useAppSelector((state) => {
const account = state.accounts.get(accountId); const account = state.accounts.get(accountId);

View File

@ -37,7 +37,6 @@ export const GIFV = forwardRef<HTMLVideoElement, Props>(
role='button' role='button'
tabIndex={0} tabIndex={0}
aria-label={alt} aria-label={alt}
title={alt}
lang={lang} lang={lang}
onClick={handleClick} onClick={handleClick}
/> />
@ -49,7 +48,6 @@ export const GIFV = forwardRef<HTMLVideoElement, Props>(
role='button' role='button'
tabIndex={0} tabIndex={0}
aria-label={alt} aria-label={alt}
title={alt}
lang={lang} lang={lang}
width={width} width={width}
height={height} height={height}

View File

@ -40,7 +40,11 @@ type KeyMatcher = (
*/ */
function just(keyName: string): KeyMatcher { function just(keyName: string): KeyMatcher {
return (event) => ({ return (event) => ({
isMatch: normalizeKey(event.key) === keyName, isMatch:
normalizeKey(event.key) === keyName &&
!event.altKey &&
!event.ctrlKey &&
!event.metaKey,
priority: hotkeyPriority.singleKey, priority: hotkeyPriority.singleKey,
}); });
} }

View File

@ -0,0 +1,63 @@
import { useState, useRef, useCallback, useId } from 'react';
import { FormattedMessage } from 'react-intl';
import Overlay from 'react-overlays/Overlay';
export const LearnMoreLink: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const accessibilityId = useId();
const [open, setOpen] = useState(false);
const triggerRef = useRef(null);
const handleClick = useCallback(() => {
setOpen(!open);
}, [open, setOpen]);
return (
<>
<button
className='link-button'
ref={triggerRef}
onClick={handleClick}
aria-expanded={open}
aria-controls={accessibilityId}
>
<FormattedMessage
id='learn_more_link.learn_more'
defaultMessage='Learn more'
/>
</button>
<Overlay
show={open}
rootClose
onHide={handleClick}
offset={[5, 5]}
placement='bottom-end'
target={triggerRef}
>
{({ props }) => (
<div
{...props}
role='region'
id={accessibilityId}
className='account__domain-pill__popout learn-more__popout dropdown-animation'
>
<div className='learn-more__popout__content'>{children}</div>
<div>
<button className='link-button' onClick={handleClick}>
<FormattedMessage
id='learn_more_link.got_it'
defaultMessage='Got it'
/>
</button>
</div>
</div>
)}
</Overlay>
</>
);
};

View File

@ -67,21 +67,28 @@ const messages = defineMessages({
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
filter: { id: 'status.filter', defaultMessage: 'Filter this post' }, filter: { id: 'status.filter', defaultMessage: 'Filter this post' },
openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' }, openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
revokeQuote: { id: 'status.revoke_quote', defaultMessage: 'Remove my post from @{name}s post' },
}); });
const mapStateToProps = (state, { status }) => ({ const mapStateToProps = (state, { status }) => {
relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]), const quotedStatusId = status.getIn(['quote', 'quoted_status']);
}); return ({
relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]),
quotedAccountId: quotedStatusId ? state.getIn(['statuses', quotedStatusId, 'account']) : null,
});
};
class StatusActionBar extends ImmutablePureComponent { class StatusActionBar extends ImmutablePureComponent {
static propTypes = { static propTypes = {
identity: identityContextPropShape, identity: identityContextPropShape,
status: ImmutablePropTypes.map.isRequired, status: ImmutablePropTypes.map.isRequired,
relationship: ImmutablePropTypes.record, relationship: ImmutablePropTypes.record,
quotedAccountId: ImmutablePropTypes.string,
onReply: PropTypes.func, onReply: PropTypes.func,
onFavourite: PropTypes.func, onFavourite: PropTypes.func,
onReblog: PropTypes.func, onReblog: PropTypes.func,
onDelete: PropTypes.func, onDelete: PropTypes.func,
onRevokeQuote: PropTypes.func,
onDirect: PropTypes.func, onDirect: PropTypes.func,
onMention: PropTypes.func, onMention: PropTypes.func,
onMute: PropTypes.func, onMute: PropTypes.func,
@ -110,6 +117,7 @@ class StatusActionBar extends ImmutablePureComponent {
updateOnProps = [ updateOnProps = [
'status', 'status',
'relationship', 'relationship',
'quotedAccountId',
'withDismiss', 'withDismiss',
]; ];
@ -190,6 +198,10 @@ class StatusActionBar extends ImmutablePureComponent {
} }
}; };
handleRevokeQuoteClick = () => {
this.props.onRevokeQuote(this.props.status);
}
handleBlockClick = () => { handleBlockClick = () => {
const { status, relationship, onBlock, onUnblock } = this.props; const { status, relationship, onBlock, onUnblock } = this.props;
const account = status.get('account'); const account = status.get('account');
@ -241,7 +253,7 @@ class StatusActionBar extends ImmutablePureComponent {
}; };
render () { render () {
const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props; const { status, relationship, quotedAccountId, intl, withDismiss, withCounters, scrollKey } = this.props;
const { signedIn, permissions } = this.props.identity; const { signedIn, permissions } = this.props.identity;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
@ -291,6 +303,10 @@ class StatusActionBar extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.handleDirectClick }); menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.handleDirectClick });
menu.push(null); menu.push(null);
if (quotedAccountId === me) {
menu.push({ text: intl.formatMessage(messages.revokeQuote, { name: account.get('username') }), action: this.handleRevokeQuoteClick, dangerous: true });
}
if (relationship && relationship.get('muting')) { if (relationship && relationship.get('muting')) {
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick }); menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick });
} else { } else {

View File

@ -15,8 +15,9 @@ import { undoStatusTranslation } from 'mastodon/actions/statuses';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import { Poll } from 'mastodon/components/poll'; import { Poll } from 'mastodon/components/poll';
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
import { autoPlayGif, isFeatureEnabled, languages as preloadedLanguages } from 'mastodon/initial_state'; import { autoPlayGif, languages as preloadedLanguages } from 'mastodon/initial_state';
import { EmojiHTML } from '../features/emoji/emoji_html'; import { EmojiHTML } from '../features/emoji/emoji_html';
import { isModernEmojiEnabled } from '../utils/environment';
const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top) const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
@ -28,7 +29,7 @@ const supportsTranslator = 'Translator' in globalThis;
* @returns {string} * @returns {string}
*/ */
export function getStatusContent(status) { export function getStatusContent(status) {
if (isFeatureEnabled('modern_emojis')) { if (isModernEmojiEnabled()) {
return status.getIn(['translation', 'content']) || status.get('content'); return status.getIn(['translation', 'content']) || status.get('content');
} }
return status.getIn(['translation', 'contentHtml']) || status.get('contentHtml'); return status.getIn(['translation', 'contentHtml']) || status.get('contentHtml');
@ -51,13 +52,13 @@ class TranslateButton extends PureComponent {
return ( return (
<div className='translate-button'> <div className='translate-button'>
<div className='translate-button__meta'>
<FormattedMessage id='status.translated_from_with' defaultMessage='Translated from {lang} using {provider}' values={{ lang: languageName, provider }} />
</div>
<button className='link-button' onClick={onClick}> <button className='link-button' onClick={onClick}>
<FormattedMessage id='status.show_original' defaultMessage='Show original' /> <FormattedMessage id='status.show_original' defaultMessage='Show original' />
</button> </button>
<div className='translate-button__meta'>
<FormattedMessage id='status.translated_from_with' defaultMessage='Translated from {lang} using {provider}' values={{ lang: languageName, provider }} />
</div>
</div> </div>
); );
} }
@ -149,6 +150,16 @@ class StatusContent extends PureComponent {
onCollapsedToggle(collapsed); onCollapsedToggle(collapsed);
} }
// Remove quote fallback link from the DOM so it doesn't
// mess with paragraph margins
if (!!status.get('quote')) {
const inlineQuote = node.querySelector('.quote-inline');
if (inlineQuote) {
inlineQuote.remove();
}
}
} }
handleMouseEnter = ({ currentTarget }) => { handleMouseEnter = ({ currentTarget }) => {

View File

@ -41,9 +41,11 @@ export default class StatusList extends ImmutablePureComponent {
}; };
componentDidMount() { componentDidMount() {
this.columnHeaderHeight = parseFloat( this.columnHeaderHeight = this.node?.node
getComputedStyle(this.node.node).getPropertyValue('--column-header-height') ? parseFloat(
) || 0; getComputedStyle(this.node.node).getPropertyValue('--column-header-height')
) || 0
: 0;
} }
getFeaturedStatusCount = () => { getFeaturedStatusCount = () => {
@ -69,8 +71,8 @@ export default class StatusList extends ImmutablePureComponent {
}; };
_selectChild = (id, index, direction) => { _selectChild = (id, index, direction) => {
const listContainer = this.node.node; const listContainer = this.node?.node;
let listItem = listContainer.querySelector( let listItem = listContainer?.querySelector(
// :nth-child uses 1-based indexing // :nth-child uses 1-based indexing
`.item-list > :nth-child(${index + 1 + direction})` `.item-list > :nth-child(${index + 1 + direction})`
); );

View File

@ -3,19 +3,15 @@ import { useEffect, useMemo } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { Link } from 'react-router-dom';
import type { Map as ImmutableMap } from 'immutable'; import type { Map as ImmutableMap } from 'immutable';
import ArticleIcon from '@/material-icons/400-24px/article.svg?react'; import { LearnMoreLink } from 'mastodon/components/learn_more_link';
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import { Icon } from 'mastodon/components/icon';
import StatusContainer from 'mastodon/containers/status_container'; import StatusContainer from 'mastodon/containers/status_container';
import type { Status } from 'mastodon/models/status'; import type { Status } from 'mastodon/models/status';
import type { RootState } from 'mastodon/store'; import type { RootState } from 'mastodon/store';
import { useAppDispatch, useAppSelector } from 'mastodon/store'; import { useAppDispatch, useAppSelector } from 'mastodon/store';
import QuoteIcon from '../../images/quote.svg?react';
import { fetchStatus } from '../actions/statuses'; import { fetchStatus } from '../actions/statuses';
import { makeGetStatus } from '../selectors'; import { makeGetStatus } from '../selectors';
@ -31,7 +27,6 @@ const QuoteWrapper: React.FC<{
'status__quote--error': isError, 'status__quote--error': isError,
})} })}
> >
<Icon id='quote' icon={QuoteIcon} className='status__quote-icon' />
{children} {children}
</div> </div>
); );
@ -45,27 +40,20 @@ const NestedQuoteLink: React.FC<{
accountId ? state.accounts.get(accountId) : undefined, accountId ? state.accounts.get(accountId) : undefined,
); );
const quoteAuthorName = account?.display_name_html; const quoteAuthorName = account?.acct;
if (!quoteAuthorName) { if (!quoteAuthorName) {
return null; return null;
} }
const quoteAuthorElement = (
<span dangerouslySetInnerHTML={{ __html: quoteAuthorName }} />
);
const quoteUrl = `/@${account.get('acct')}/${status.get('id') as string}`;
return ( return (
<Link to={quoteUrl} className='status__quote-author-button'> <div className='status__quote-author-button'>
<FormattedMessage <FormattedMessage
id='status.quote_post_author' id='status.quote_post_author'
defaultMessage='Post by {name}' defaultMessage='Quoted a post by @{name}'
values={{ name: quoteAuthorElement }} values={{ name: quoteAuthorName }}
/> />
<Icon id='chevron_right' icon={ChevronRightIcon} /> </div>
<Icon id='article' icon={ArticleIcon} />
</Link>
); );
}; };
@ -112,39 +100,42 @@ export const QuotedStatus: React.FC<{
defaultMessage='Hidden due to one of your filters' defaultMessage='Hidden due to one of your filters'
/> />
); );
} else if (quoteState === 'deleted') {
quoteError = (
<FormattedMessage
id='status.quote_error.removed'
defaultMessage='This post was removed by its author.'
/>
);
} else if (quoteState === 'unauthorized') {
quoteError = (
<FormattedMessage
id='status.quote_error.unauthorized'
defaultMessage='This post cannot be displayed as you are not authorized to view it.'
/>
);
} else if (quoteState === 'pending') { } else if (quoteState === 'pending') {
quoteError = ( quoteError = (
<FormattedMessage <>
id='status.quote_error.pending_approval' <FormattedMessage
defaultMessage='This post is pending approval from the original author.' id='status.quote_error.pending_approval'
/> defaultMessage='Post pending'
/>
<LearnMoreLink>
<h6>
<FormattedMessage
id='status.quote_error.pending_approval_popout.title'
defaultMessage='Pending quote? Remain calm'
/>
</h6>
<p>
<FormattedMessage
id='status.quote_error.pending_approval_popout.body'
defaultMessage='Quotes shared across the Fediverse may take time to display, as different servers have different protocols.'
/>
</p>
</LearnMoreLink>
</>
); );
} else if (quoteState === 'rejected' || quoteState === 'revoked') { } else if (
!status ||
!quotedStatusId ||
quoteState === 'deleted' ||
quoteState === 'rejected' ||
quoteState === 'revoked' ||
quoteState === 'unauthorized'
) {
quoteError = ( quoteError = (
<FormattedMessage <FormattedMessage
id='status.quote_error.rejected' id='status.quote_error.not_available'
defaultMessage='This post cannot be displayed as the original author does not allow it to be quoted.' defaultMessage='Post unavailable'
/>
);
} else if (!status || !quotedStatusId) {
quoteError = (
<FormattedMessage
id='status.quote_error.not_found'
defaultMessage='This post cannot be displayed.'
/> />
); );
} }
@ -168,7 +159,7 @@ export const QuotedStatus: React.FC<{
isQuotedPost isQuotedPost
id={quotedStatusId} id={quotedStatusId}
contextType={contextType} contextType={contextType}
avatarSize={40} avatarSize={32}
> >
{canRenderChildQuote && ( {canRenderChildQuote && (
<QuotedStatus <QuotedStatus

View File

@ -111,6 +111,10 @@ const mapDispatchToProps = (dispatch, { contextType }) => ({
} }
}, },
onRevokeQuote (status) {
dispatch(openModal({ modalType: 'CONFIRM_REVOKE_QUOTE', modalProps: { statusId: status.get('id'), quotedStatusId: status.getIn(['quote', 'quoted_status']) }}));
},
onEdit (status) { onEdit (status) {
dispatch((_, getState) => { dispatch((_, getState) => {
let state = getState(); let state = getState();

View File

@ -0,0 +1,139 @@
import { IDBFactory } from 'fake-indexeddb';
import { unicodeEmojiFactory } from '@/testing/factories';
import {
putEmojiData,
loadEmojiByHexcode,
searchEmojisByHexcodes,
searchEmojisByTag,
testClear,
testGet,
} from './database';
describe('emoji database', () => {
afterEach(() => {
testClear();
indexedDB = new IDBFactory();
});
describe('putEmojiData', () => {
test('adds to loaded locales', async () => {
const { loadedLocales } = await testGet();
expect(loadedLocales).toHaveLength(0);
await putEmojiData([], 'en');
expect(loadedLocales).toContain('en');
});
test('loads emoji into indexedDB', async () => {
await putEmojiData([unicodeEmojiFactory()], 'en');
const { db } = await testGet();
await expect(db.get('en', 'test')).resolves.toEqual(
unicodeEmojiFactory(),
);
});
});
describe('loadEmojiByHexcode', () => {
test('throws if the locale is not loaded', async () => {
await expect(loadEmojiByHexcode('en', 'test')).rejects.toThrowError(
'Locale en',
);
});
test('retrieves the emoji', async () => {
await putEmojiData([unicodeEmojiFactory()], 'en');
await expect(loadEmojiByHexcode('test', 'en')).resolves.toEqual(
unicodeEmojiFactory(),
);
});
test('returns undefined if not found', async () => {
await putEmojiData([], 'en');
await expect(loadEmojiByHexcode('test', 'en')).resolves.toBeUndefined();
});
});
describe('searchEmojisByHexcodes', () => {
const data = [
unicodeEmojiFactory({ hexcode: 'not a number' }),
unicodeEmojiFactory({ hexcode: '1' }),
unicodeEmojiFactory({ hexcode: '2' }),
unicodeEmojiFactory({ hexcode: '3' }),
unicodeEmojiFactory({ hexcode: 'another not a number' }),
];
beforeEach(async () => {
await putEmojiData(data, 'en');
});
test('finds emoji in consecutive range', async () => {
const actual = await searchEmojisByHexcodes(['1', '2', '3'], 'en');
expect(actual).toHaveLength(3);
});
test('finds emoji in split range', async () => {
const actual = await searchEmojisByHexcodes(['1', '3'], 'en');
expect(actual).toHaveLength(2);
expect(actual).toContainEqual(data.at(1));
expect(actual).toContainEqual(data.at(3));
});
test('finds emoji with non-numeric range', async () => {
const actual = await searchEmojisByHexcodes(
['3', 'not a number', '1'],
'en',
);
expect(actual).toHaveLength(3);
expect(actual).toContainEqual(data.at(0));
expect(actual).toContainEqual(data.at(1));
expect(actual).toContainEqual(data.at(3));
});
test('not found emoji are not returned', async () => {
const actual = await searchEmojisByHexcodes(['not found'], 'en');
expect(actual).toHaveLength(0);
});
test('only found emojis are returned', async () => {
const actual = await searchEmojisByHexcodes(
['another not a number', 'not found'],
'en',
);
expect(actual).toHaveLength(1);
expect(actual).toContainEqual(data.at(4));
});
});
describe('searchEmojisByTag', () => {
const data = [
unicodeEmojiFactory({ hexcode: 'test1', tags: ['test 1'] }),
unicodeEmojiFactory({
hexcode: 'test2',
tags: ['test 2', 'something else'],
}),
unicodeEmojiFactory({ hexcode: 'test3', tags: ['completely different'] }),
];
beforeEach(async () => {
await putEmojiData(data, 'en');
});
test('finds emojis with tag', async () => {
const actual = await searchEmojisByTag('test 1', 'en');
expect(actual).toHaveLength(1);
expect(actual).toContainEqual(data.at(0));
});
test('finds emojis starting with tag', async () => {
const actual = await searchEmojisByTag('test', 'en');
expect(actual).toHaveLength(2);
expect(actual).not.toContainEqual(data.at(2));
});
test('does not find emojis ending with tag', async () => {
const actual = await searchEmojisByTag('else', 'en');
expect(actual).toHaveLength(0);
});
test('finds nothing with invalid tag', async () => {
const actual = await searchEmojisByTag('not found', 'en');
expect(actual).toHaveLength(0);
});
});
});

View File

@ -9,6 +9,7 @@ import type {
UnicodeEmojiData, UnicodeEmojiData,
LocaleOrCustom, LocaleOrCustom,
} from './types'; } from './types';
import { emojiLogger } from './utils';
interface EmojiDB extends LocaleTables, DBSchema { interface EmojiDB extends LocaleTables, DBSchema {
custom: { custom: {
@ -36,40 +37,63 @@ interface LocaleTable {
} }
type LocaleTables = Record<Locale, LocaleTable>; type LocaleTables = Record<Locale, LocaleTable>;
type Database = IDBPDatabase<EmojiDB>;
const SCHEMA_VERSION = 1; const SCHEMA_VERSION = 1;
let db: IDBPDatabase<EmojiDB> | null = null; const loadedLocales = new Set<Locale>();
async function loadDB() { const log = emojiLogger('database');
if (db) {
return db;
}
db = await openDB<EmojiDB>('mastodon-emoji', SCHEMA_VERSION, {
upgrade(database) {
const customTable = database.createObjectStore('custom', {
keyPath: 'shortcode',
autoIncrement: false,
});
customTable.createIndex('category', 'category');
database.createObjectStore('etags'); // Loads the database in a way that ensures it's only loaded once.
const loadDB = (() => {
let dbPromise: Promise<Database> | null = null;
for (const locale of SUPPORTED_LOCALES) { // Actually load the DB.
const localeTable = database.createObjectStore(locale, { async function initDB() {
keyPath: 'hexcode', const db = await openDB<EmojiDB>('mastodon-emoji', SCHEMA_VERSION, {
upgrade(database) {
const customTable = database.createObjectStore('custom', {
keyPath: 'shortcode',
autoIncrement: false, autoIncrement: false,
}); });
localeTable.createIndex('group', 'group'); customTable.createIndex('category', 'category');
localeTable.createIndex('label', 'label');
localeTable.createIndex('order', 'order'); database.createObjectStore('etags');
localeTable.createIndex('tags', 'tags', { multiEntry: true });
} for (const locale of SUPPORTED_LOCALES) {
}, const localeTable = database.createObjectStore(locale, {
}); keyPath: 'hexcode',
return db; autoIncrement: false,
} });
localeTable.createIndex('group', 'group');
localeTable.createIndex('label', 'label');
localeTable.createIndex('order', 'order');
localeTable.createIndex('tags', 'tags', { multiEntry: true });
}
},
});
await syncLocales(db);
return db;
}
// Loads the database, or returns the existing promise if it hasn't resolved yet.
const loadPromise = async (): Promise<Database> => {
if (dbPromise) {
return dbPromise;
}
dbPromise = initDB();
return dbPromise;
};
// Special way to reset the database, used for unit testing.
loadPromise.reset = () => {
dbPromise = null;
};
return loadPromise;
})();
export async function putEmojiData(emojis: UnicodeEmojiData[], locale: Locale) { export async function putEmojiData(emojis: UnicodeEmojiData[], locale: Locale) {
loadedLocales.add(locale);
const db = await loadDB(); const db = await loadDB();
const trx = db.transaction(locale, 'readwrite'); const trx = db.transaction(locale, 'readwrite');
await Promise.all(emojis.map((emoji) => trx.store.put(emoji))); await Promise.all(emojis.map((emoji) => trx.store.put(emoji)));
@ -86,15 +110,15 @@ export async function putCustomEmojiData(emojis: CustomEmojiData[]) {
export async function putLatestEtag(etag: string, localeString: string) { export async function putLatestEtag(etag: string, localeString: string) {
const locale = toSupportedLocaleOrCustom(localeString); const locale = toSupportedLocaleOrCustom(localeString);
const db = await loadDB(); const db = await loadDB();
return db.put('etags', etag, locale); await db.put('etags', etag, locale);
} }
export async function searchEmojiByHexcode( export async function loadEmojiByHexcode(
hexcode: string, hexcode: string,
localeString: string, localeString: string,
) { ) {
const locale = toSupportedLocale(localeString);
const db = await loadDB(); const db = await loadDB();
const locale = toLoadedLocale(localeString);
return db.get(locale, hexcode); return db.get(locale, hexcode);
} }
@ -102,45 +126,39 @@ export async function searchEmojisByHexcodes(
hexcodes: string[], hexcodes: string[],
localeString: string, localeString: string,
) { ) {
const locale = toSupportedLocale(localeString);
const db = await loadDB(); const db = await loadDB();
return db.getAll( const locale = toLoadedLocale(localeString);
const sortedCodes = hexcodes.toSorted();
const results = await db.getAll(
locale, locale,
IDBKeyRange.bound(hexcodes[0], hexcodes[hexcodes.length - 1]), IDBKeyRange.bound(sortedCodes.at(0), sortedCodes.at(-1)),
); );
return results.filter((emoji) => hexcodes.includes(emoji.hexcode));
} }
export async function searchEmojiByTag(tag: string, localeString: string) { export async function searchEmojisByTag(tag: string, localeString: string) {
const locale = toSupportedLocale(localeString);
const range = IDBKeyRange.only(tag.toLowerCase());
const db = await loadDB(); const db = await loadDB();
const locale = toLoadedLocale(localeString);
const range = IDBKeyRange.bound(
tag.toLowerCase(),
`${tag.toLowerCase()}\uffff`,
);
return db.getAllFromIndex(locale, 'tags', range); return db.getAllFromIndex(locale, 'tags', range);
} }
export async function searchCustomEmojiByShortcode(shortcode: string) { export async function loadCustomEmojiByShortcode(shortcode: string) {
const db = await loadDB(); const db = await loadDB();
return db.get('custom', shortcode); return db.get('custom', shortcode);
} }
export async function searchCustomEmojisByShortcodes(shortcodes: string[]) { export async function searchCustomEmojisByShortcodes(shortcodes: string[]) {
const db = await loadDB(); const db = await loadDB();
return db.getAll( const sortedCodes = shortcodes.toSorted();
const results = await db.getAll(
'custom', 'custom',
IDBKeyRange.bound(shortcodes[0], shortcodes[shortcodes.length - 1]), IDBKeyRange.bound(sortedCodes.at(0), sortedCodes.at(-1)),
); );
} return results.filter((emoji) => shortcodes.includes(emoji.shortcode));
export async function findMissingLocales(localeStrings: string[]) {
const locales = new Set(localeStrings.map(toSupportedLocale));
const missingLocales: Locale[] = [];
const db = await loadDB();
for (const locale of locales) {
const rowCount = await db.count(locale);
if (!rowCount) {
missingLocales.push(locale);
}
}
return missingLocales;
} }
export async function loadLatestEtag(localeString: string) { export async function loadLatestEtag(localeString: string) {
@ -153,3 +171,51 @@ export async function loadLatestEtag(localeString: string) {
const etag = await db.get('etags', locale); const etag = await db.get('etags', locale);
return etag ?? null; return etag ?? null;
} }
// Private functions
async function syncLocales(db: Database) {
const locales = await Promise.all(
SUPPORTED_LOCALES.map(
async (locale) =>
[locale, await hasLocale(locale, db)] satisfies [Locale, boolean],
),
);
for (const [locale, loaded] of locales) {
if (loaded) {
loadedLocales.add(locale);
} else {
loadedLocales.delete(locale);
}
}
log('Loaded %d locales: %o', loadedLocales.size, loadedLocales);
}
function toLoadedLocale(localeString: string) {
const locale = toSupportedLocale(localeString);
if (localeString !== locale) {
log(`Locale ${locale} is different from provided ${localeString}`);
}
if (!loadedLocales.has(locale)) {
throw new Error(`Locale ${locale} is not loaded in emoji database`);
}
return locale;
}
async function hasLocale(locale: Locale, db: Database): Promise<boolean> {
if (loadedLocales.has(locale)) {
return true;
}
const rowCount = await db.count(locale);
return !!rowCount;
}
// Testing helpers
export async function testGet() {
const db = await loadDB();
return { db, loadedLocales };
}
export function testClear() {
loadedLocales.clear();
loadDB.reset();
}

View File

@ -1,81 +1,48 @@
import type { HTMLAttributes } from 'react'; import type { ComponentPropsWithoutRef, ElementType } from 'react';
import { useEffect, useMemo, useState } from 'react';
import type { List as ImmutableList } from 'immutable'; import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
import { isList } from 'immutable';
import type { ApiCustomEmojiJSON } from '@/mastodon/api_types/custom_emoji'; import { useEmojify } from './hooks';
import { isFeatureEnabled } from '@/mastodon/initial_state'; import type { CustomEmojiMapArg } from './types';
import type { CustomEmoji } from '@/mastodon/models/custom_emoji';
import { useEmojiAppState } from './hooks'; type EmojiHTMLProps<Element extends ElementType = 'div'> = Omit<
import { emojifyElement } from './render'; ComponentPropsWithoutRef<Element>,
import type { ExtraCustomEmojiMap } from './types';
type EmojiHTMLProps = Omit<
HTMLAttributes<HTMLDivElement>,
'dangerouslySetInnerHTML' 'dangerouslySetInnerHTML'
> & { > & {
htmlString: string; htmlString: string;
extraEmojis?: ExtraCustomEmojiMap | ImmutableList<CustomEmoji>; extraEmojis?: CustomEmojiMapArg;
as?: Element;
}; };
export const EmojiHTML: React.FC<EmojiHTMLProps> = ({ export const ModernEmojiHTML = <Element extends ElementType>({
htmlString,
extraEmojis, extraEmojis,
htmlString,
as: asElement, // Rename for syntax highlighting
...props ...props
}) => { }: EmojiHTMLProps<Element>) => {
if (isFeatureEnabled('modern_emojis')) { const Wrapper = asElement ?? 'div';
return ( const emojifiedHtml = useEmojify(htmlString, extraEmojis);
<ModernEmojiHTML
htmlString={htmlString}
extraEmojis={extraEmojis}
{...props}
/>
);
}
return <div dangerouslySetInnerHTML={{ __html: htmlString }} {...props} />;
};
const ModernEmojiHTML: React.FC<EmojiHTMLProps> = ({ if (emojifiedHtml === null) {
extraEmojis: rawEmojis,
htmlString: text,
...props
}) => {
const appState = useEmojiAppState();
const [innerHTML, setInnerHTML] = useState('');
const extraEmojis: ExtraCustomEmojiMap = useMemo(() => {
if (!rawEmojis) {
return {};
}
if (isList(rawEmojis)) {
return (
rawEmojis.toJS() as ApiCustomEmojiJSON[]
).reduce<ExtraCustomEmojiMap>(
(acc, emoji) => ({ ...acc, [emoji.shortcode]: emoji }),
{},
);
}
return rawEmojis;
}, [rawEmojis]);
useEffect(() => {
if (!text) {
return;
}
const cb = async () => {
const div = document.createElement('div');
div.innerHTML = text;
const ele = await emojifyElement(div, appState, extraEmojis);
setInnerHTML(ele.innerHTML);
};
void cb();
}, [text, appState, extraEmojis]);
if (!innerHTML) {
return null; return null;
} }
return <div {...props} dangerouslySetInnerHTML={{ __html: innerHTML }} />; return (
<Wrapper {...props} dangerouslySetInnerHTML={{ __html: emojifiedHtml }} />
);
};
export const EmojiHTML = <Element extends ElementType>(
props: EmojiHTMLProps<Element>,
) => {
if (isModernEmojiEnabled()) {
return <ModernEmojiHTML {...props} />;
}
const Wrapper = props.as ?? 'div';
return (
<Wrapper
{...props}
dangerouslySetInnerHTML={{ __html: props.htmlString }}
/>
);
}; };

View File

@ -1,45 +0,0 @@
import { useEffect, useState } from 'react';
import { useEmojiAppState } from './hooks';
import { emojifyText } from './render';
interface EmojiTextProps {
text: string;
}
export const EmojiText: React.FC<EmojiTextProps> = ({ text }) => {
const appState = useEmojiAppState();
const [rendered, setRendered] = useState<(string | HTMLImageElement)[]>([]);
useEffect(() => {
const cb = async () => {
const rendered = await emojifyText(text, appState);
setRendered(rendered ?? []);
};
void cb();
}, [text, appState]);
if (rendered.length === 0) {
return null;
}
return (
<>
{rendered.map((fragment, index) => {
if (typeof fragment === 'string') {
return <span key={index}>{fragment}</span>;
}
return (
<img
key={index}
draggable='false'
src={fragment.src}
alt={fragment.alt}
title={fragment.title}
className={fragment.className}
/>
);
})}
</>
);
};

View File

@ -1,8 +1,64 @@
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import { isList } from 'immutable';
import type { ApiCustomEmojiJSON } from '@/mastodon/api_types/custom_emoji';
import { useAppSelector } from '@/mastodon/store'; import { useAppSelector } from '@/mastodon/store';
import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
import { toSupportedLocale } from './locale'; import { toSupportedLocale } from './locale';
import { determineEmojiMode } from './mode'; import { determineEmojiMode } from './mode';
import type { EmojiAppState } from './types'; import type {
CustomEmojiMapArg,
EmojiAppState,
ExtraCustomEmojiMap,
} from './types';
import { stringHasAnyEmoji } from './utils';
export function useEmojify(text: string, extraEmojis?: CustomEmojiMapArg) {
const [emojifiedText, setEmojifiedText] = useState<string | null>(null);
const appState = useEmojiAppState();
const extra: ExtraCustomEmojiMap = useMemo(() => {
if (!extraEmojis) {
return {};
}
if (isList(extraEmojis)) {
return (
extraEmojis.toJS() as ApiCustomEmojiJSON[]
).reduce<ExtraCustomEmojiMap>(
(acc, emoji) => ({ ...acc, [emoji.shortcode]: emoji }),
{},
);
}
return extraEmojis;
}, [extraEmojis]);
const emojify = useCallback(
async (input: string) => {
const wrapper = document.createElement('div');
wrapper.innerHTML = input;
const { emojifyElement } = await import('./render');
const result = await emojifyElement(wrapper, appState, extra);
if (result) {
setEmojifiedText(result.innerHTML);
} else {
setEmojifiedText(input);
}
},
[appState, extra],
);
useLayoutEffect(() => {
if (isModernEmojiEnabled() && !!text.trim() && stringHasAnyEmoji(text)) {
void emojify(text);
} else {
// If no emoji or we don't want to render, fall back.
setEmojifiedText(text);
}
}, [emojify, text]);
return emojifiedText;
}
export function useEmojiAppState(): EmojiAppState { export function useEmojiAppState(): EmojiAppState {
const locale = useAppSelector((state) => const locale = useAppSelector((state) =>
@ -12,5 +68,10 @@ export function useEmojiAppState(): EmojiAppState {
determineEmojiMode(state.meta.get('emoji_style') as string), determineEmojiMode(state.meta.get('emoji_style') as string),
); );
return { currentLocale: locale, locales: [locale], mode }; return {
currentLocale: locale,
locales: [locale],
mode,
darkTheme: document.body.classList.contains('theme-default'),
};
} }

View File

@ -1,17 +1,21 @@
import initialState from '@/mastodon/initial_state'; import initialState from '@/mastodon/initial_state';
import { loadWorker } from '@/mastodon/utils/workers';
import { toSupportedLocale } from './locale'; import { toSupportedLocale } from './locale';
import { emojiLogger } from './utils';
const userLocale = toSupportedLocale(initialState?.meta.locale ?? 'en'); const userLocale = toSupportedLocale(initialState?.meta.locale ?? 'en');
let worker: Worker | null = null; let worker: Worker | null = null;
export async function initializeEmoji() { const log = emojiLogger('index');
export function initializeEmoji() {
log('initializing emojis');
if (!worker && 'Worker' in window) { if (!worker && 'Worker' in window) {
try { try {
worker = new Worker(new URL('./worker', import.meta.url), { worker = loadWorker(new URL('./worker', import.meta.url), {
type: 'module', type: 'module',
credentials: 'omit',
}); });
} catch (err) { } catch (err) {
console.warn('Error creating web worker:', err); console.warn('Error creating web worker:', err);
@ -21,9 +25,16 @@ export async function initializeEmoji() {
if (worker) { if (worker) {
// Assign worker to const to make TS happy inside the event listener. // Assign worker to const to make TS happy inside the event listener.
const thisWorker = worker; const thisWorker = worker;
const timeoutId = setTimeout(() => {
log('worker is not ready after timeout');
worker = null;
void fallbackLoad();
}, 500);
thisWorker.addEventListener('message', (event: MessageEvent<string>) => { thisWorker.addEventListener('message', (event: MessageEvent<string>) => {
const { data: message } = event; const { data: message } = event;
if (message === 'ready') { if (message === 'ready') {
log('worker ready, loading data');
clearTimeout(timeoutId);
thisWorker.postMessage('custom'); thisWorker.postMessage('custom');
void loadEmojiLocale(userLocale); void loadEmojiLocale(userLocale);
// Load English locale as well, because people are still used to // Load English locale as well, because people are still used to
@ -31,15 +42,22 @@ export async function initializeEmoji() {
if (userLocale !== 'en') { if (userLocale !== 'en') {
void loadEmojiLocale('en'); void loadEmojiLocale('en');
} }
} else {
log('got worker message: %s', message);
} }
}); });
} else { } else {
const { importCustomEmojiData } = await import('./loader'); void fallbackLoad();
await importCustomEmojiData(); }
await loadEmojiLocale(userLocale); }
if (userLocale !== 'en') {
await loadEmojiLocale('en'); async function fallbackLoad() {
} log('falling back to main thread for loading');
const { importCustomEmojiData } = await import('./loader');
await importCustomEmojiData();
await loadEmojiLocale(userLocale);
if (userLocale !== 'en') {
await loadEmojiLocale('en');
} }
} }

View File

@ -2,7 +2,6 @@ import { flattenEmojiData } from 'emojibase';
import type { CompactEmoji, FlatCompactEmoji } from 'emojibase'; import type { CompactEmoji, FlatCompactEmoji } from 'emojibase';
import type { ApiCustomEmojiJSON } from '@/mastodon/api_types/custom_emoji'; import type { ApiCustomEmojiJSON } from '@/mastodon/api_types/custom_emoji';
import { isDevelopment } from '@/mastodon/utils/environment';
import { import {
putEmojiData, putEmojiData,
@ -12,6 +11,9 @@ import {
} from './database'; } from './database';
import { toSupportedLocale, toSupportedLocaleOrCustom } from './locale'; import { toSupportedLocale, toSupportedLocaleOrCustom } from './locale';
import type { LocaleOrCustom } from './types'; import type { LocaleOrCustom } from './types';
import { emojiLogger } from './utils';
const log = emojiLogger('loader');
export async function importEmojiData(localeString: string) { export async function importEmojiData(localeString: string) {
const locale = toSupportedLocale(localeString); const locale = toSupportedLocale(localeString);
@ -20,6 +22,7 @@ export async function importEmojiData(localeString: string) {
return; return;
} }
const flattenedEmojis: FlatCompactEmoji[] = flattenEmojiData(emojis); const flattenedEmojis: FlatCompactEmoji[] = flattenEmojiData(emojis);
log('loaded %d for %s locale', flattenedEmojis.length, locale);
await putEmojiData(flattenedEmojis, locale); await putEmojiData(flattenedEmojis, locale);
} }
@ -28,6 +31,7 @@ export async function importCustomEmojiData() {
if (!emojis) { if (!emojis) {
return; return;
} }
log('loaded %d custom emojis', emojis.length);
await putCustomEmojiData(emojis); await putCustomEmojiData(emojis);
} }
@ -36,15 +40,18 @@ async function fetchAndCheckEtag<ResultType extends object[]>(
): Promise<ResultType | null> { ): Promise<ResultType | null> {
const locale = toSupportedLocaleOrCustom(localeOrCustom); const locale = toSupportedLocaleOrCustom(localeOrCustom);
let uri: string; // Use location.origin as this script may be loaded from a CDN domain.
const url = new URL(location.origin);
if (locale === 'custom') { if (locale === 'custom') {
uri = '/api/v1/custom_emojis'; url.pathname = '/api/v1/custom_emojis';
} else { } else {
uri = `/packs${isDevelopment() ? '-dev' : ''}/emoji/${locale}.json`; // This doesn't use isDevelopment() as that module loads initial state
// which breaks workers, as they cannot access the DOM.
url.pathname = `/packs${import.meta.env.DEV ? '-dev' : ''}/emoji/${locale}.json`;
} }
const oldEtag = await loadLatestEtag(locale); const oldEtag = await loadLatestEtag(locale);
const response = await fetch(uri, { const response = await fetch(url, {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'If-None-Match': oldEtag ?? '', // Send the old ETag to check for modifications 'If-None-Match': oldEtag ?? '', // Send the old ETag to check for modifications

View File

@ -1,94 +1,184 @@
import { customEmojiFactory, unicodeEmojiFactory } from '@/testing/factories';
import { import {
EMOJI_MODE_NATIVE, EMOJI_MODE_NATIVE,
EMOJI_MODE_NATIVE_WITH_FLAGS, EMOJI_MODE_NATIVE_WITH_FLAGS,
EMOJI_MODE_TWEMOJI, EMOJI_MODE_TWEMOJI,
} from './constants'; } from './constants';
import { emojifyElement, tokenizeText } from './render'; import * as db from './database';
import type { CustomEmojiData, UnicodeEmojiData } from './types'; import {
emojifyElement,
emojifyText,
testCacheClear,
tokenizeText,
} from './render';
import type { EmojiAppState, ExtraCustomEmojiMap } from './types';
vitest.mock('./database', () => ({ function mockDatabase() {
searchCustomEmojisByShortcodes: vitest.fn( return {
() => searchCustomEmojisByShortcodes: vi
[ .spyOn(db, 'searchCustomEmojisByShortcodes')
{ .mockResolvedValue([customEmojiFactory()]),
shortcode: 'custom', searchEmojisByHexcodes: vi
static_url: 'emoji/static', .spyOn(db, 'searchEmojisByHexcodes')
url: 'emoji/custom', .mockResolvedValue([
category: 'test', unicodeEmojiFactory({
visible_in_picker: true,
},
] satisfies CustomEmojiData[],
),
searchEmojisByHexcodes: vitest.fn(
() =>
[
{
hexcode: '1F60A', hexcode: '1F60A',
group: 0,
label: 'smiling face with smiling eyes', label: 'smiling face with smiling eyes',
order: 0,
tags: ['smile', 'happy'],
unicode: '😊', unicode: '😊',
}, }),
{ unicodeEmojiFactory({
hexcode: '1F1EA-1F1FA', hexcode: '1F1EA-1F1FA',
group: 0,
label: 'flag-eu', label: 'flag-eu',
order: 0,
tags: ['flag', 'european union'],
unicode: '🇪🇺', unicode: '🇪🇺',
}, }),
] satisfies UnicodeEmojiData[], ]),
), };
findMissingLocales: vitest.fn(() => []), }
}));
const expectedSmileImage =
'<img draggable="false" class="emojione" alt="😊" title="smiling face with smiling eyes" src="/emoji/1f60a.svg">';
const expectedFlagImage =
'<img draggable="false" class="emojione" alt="🇪🇺" title="flag-eu" src="/emoji/1f1ea-1f1fa.svg">';
const expectedCustomEmojiImage =
'<img draggable="false" class="emojione custom-emoji" alt=":custom:" title=":custom:" src="emoji/custom/static" data-original="emoji/custom" data-static="emoji/custom/static">';
const expectedRemoteCustomEmojiImage =
'<img draggable="false" class="emojione custom-emoji" alt=":remote:" title=":remote:" src="remote.social/static" data-original="remote.social/custom" data-static="remote.social/static">';
const mockExtraCustom: ExtraCustomEmojiMap = {
remote: {
shortcode: 'remote',
static_url: 'remote.social/static',
url: 'remote.social/custom',
},
};
function testAppState(state: Partial<EmojiAppState> = {}) {
return {
locales: ['en'],
mode: EMOJI_MODE_TWEMOJI,
currentLocale: 'en',
darkTheme: false,
...state,
} satisfies EmojiAppState;
}
describe('emojifyElement', () => { describe('emojifyElement', () => {
const testElement = document.createElement('div'); function testElement(text = '<p>Hello 😊🇪🇺!</p><p>:custom:</p>') {
testElement.innerHTML = '<p>Hello 😊🇪🇺!</p><p>:custom:</p>'; const testElement = document.createElement('div');
testElement.innerHTML = text;
const expectedSmileImage = return testElement;
'<img draggable="false" class="emojione" alt="😊" title="smiling face with smiling eyes" src="/emoji/1f60a.svg">';
const expectedFlagImage =
'<img draggable="false" class="emojione" alt="🇪🇺" title="flag-eu" src="/emoji/1f1ea-1f1fa.svg">';
const expectedCustomEmojiImage =
'<img draggable="false" class="emojione custom-emoji" alt=":custom:" title=":custom:" src="emoji/static" data-original="emoji/custom" data-static="emoji/static">';
function cloneTestElement() {
return testElement.cloneNode(true) as HTMLElement;
} }
afterEach(() => {
testCacheClear();
vi.restoreAllMocks();
});
test('caches element rendering results', async () => {
const { searchCustomEmojisByShortcodes, searchEmojisByHexcodes } =
mockDatabase();
await emojifyElement(testElement(), testAppState());
await emojifyElement(testElement(), testAppState());
await emojifyElement(testElement(), testAppState());
expect(searchEmojisByHexcodes).toHaveBeenCalledExactlyOnceWith(
['1F1EA-1F1FA', '1F60A'],
'en',
);
expect(searchCustomEmojisByShortcodes).toHaveBeenCalledExactlyOnceWith([
'custom',
]);
});
test('emojifies custom emoji in native mode', async () => { test('emojifies custom emoji in native mode', async () => {
const emojifiedElement = await emojifyElement(cloneTestElement(), { const { searchEmojisByHexcodes } = mockDatabase();
locales: ['en'], const actual = await emojifyElement(
mode: EMOJI_MODE_NATIVE, testElement(),
currentLocale: 'en', testAppState({ mode: EMOJI_MODE_NATIVE }),
}); );
expect(emojifiedElement.innerHTML).toBe( assert(actual);
expect(actual.innerHTML).toBe(
`<p>Hello 😊🇪🇺!</p><p>${expectedCustomEmojiImage}</p>`, `<p>Hello 😊🇪🇺!</p><p>${expectedCustomEmojiImage}</p>`,
); );
expect(searchEmojisByHexcodes).not.toHaveBeenCalled();
}); });
test('emojifies flag emoji in native-with-flags mode', async () => { test('emojifies flag emoji in native-with-flags mode', async () => {
const emojifiedElement = await emojifyElement(cloneTestElement(), { const { searchEmojisByHexcodes } = mockDatabase();
locales: ['en'], const actual = await emojifyElement(
mode: EMOJI_MODE_NATIVE_WITH_FLAGS, testElement(),
currentLocale: 'en', testAppState({ mode: EMOJI_MODE_NATIVE_WITH_FLAGS }),
}); );
expect(emojifiedElement.innerHTML).toBe( assert(actual);
expect(actual.innerHTML).toBe(
`<p>Hello 😊${expectedFlagImage}!</p><p>${expectedCustomEmojiImage}</p>`, `<p>Hello 😊${expectedFlagImage}!</p><p>${expectedCustomEmojiImage}</p>`,
); );
expect(searchEmojisByHexcodes).toHaveBeenCalledOnce();
}); });
test('emojifies everything in twemoji mode', async () => { test('emojifies everything in twemoji mode', async () => {
const emojifiedElement = await emojifyElement(cloneTestElement(), { const { searchCustomEmojisByShortcodes, searchEmojisByHexcodes } =
locales: ['en'], mockDatabase();
mode: EMOJI_MODE_TWEMOJI, const actual = await emojifyElement(testElement(), testAppState());
currentLocale: 'en', assert(actual);
}); expect(actual.innerHTML).toBe(
expect(emojifiedElement.innerHTML).toBe(
`<p>Hello ${expectedSmileImage}${expectedFlagImage}!</p><p>${expectedCustomEmojiImage}</p>`, `<p>Hello ${expectedSmileImage}${expectedFlagImage}!</p><p>${expectedCustomEmojiImage}</p>`,
); );
expect(searchEmojisByHexcodes).toHaveBeenCalledOnce();
expect(searchCustomEmojisByShortcodes).toHaveBeenCalledOnce();
});
test('emojifies with provided custom emoji', async () => {
const { searchCustomEmojisByShortcodes, searchEmojisByHexcodes } =
mockDatabase();
const actual = await emojifyElement(
testElement('<p>hi :remote:</p>'),
testAppState(),
mockExtraCustom,
);
assert(actual);
expect(actual.innerHTML).toBe(
`<p>hi ${expectedRemoteCustomEmojiImage}</p>`,
);
expect(searchEmojisByHexcodes).not.toHaveBeenCalled();
expect(searchCustomEmojisByShortcodes).not.toHaveBeenCalled();
});
test('returns null when no emoji are found', async () => {
mockDatabase();
const actual = await emojifyElement(
testElement('<p>here is just text :)</p>'),
testAppState(),
);
expect(actual).toBeNull();
});
});
describe('emojifyText', () => {
test('returns original input when no emoji are in string', async () => {
const actual = await emojifyText('nothing here', testAppState());
expect(actual).toBe('nothing here');
});
test('renders Unicode emojis to twemojis', async () => {
mockDatabase();
const actual = await emojifyText('Hello 😊🇪🇺!', testAppState());
expect(actual).toBe(`Hello ${expectedSmileImage}${expectedFlagImage}!`);
});
test('renders custom emojis', async () => {
mockDatabase();
const actual = await emojifyText('Hello :custom:!', testAppState());
expect(actual).toBe(`Hello ${expectedCustomEmojiImage}!`);
});
test('renders provided extra emojis', async () => {
const actual = await emojifyText(
'remote emoji :remote:',
testAppState(),
mockExtraCustom,
);
expect(actual).toBe(`remote emoji ${expectedRemoteCustomEmojiImage}`);
}); });
}); });

View File

@ -1,8 +1,7 @@
import type { Locale } from 'emojibase';
import EMOJI_REGEX from 'emojibase-regex/emoji-loose';
import { autoPlayGif } from '@/mastodon/initial_state'; import { autoPlayGif } from '@/mastodon/initial_state';
import { createLimitedCache } from '@/mastodon/utils/cache';
import { assetHost } from '@/mastodon/utils/config'; import { assetHost } from '@/mastodon/utils/config';
import * as perf from '@/mastodon/utils/performance';
import { import {
EMOJI_MODE_NATIVE, EMOJI_MODE_NATIVE,
@ -12,11 +11,9 @@ import {
EMOJI_STATE_MISSING, EMOJI_STATE_MISSING,
} from './constants'; } from './constants';
import { import {
findMissingLocales,
searchCustomEmojisByShortcodes, searchCustomEmojisByShortcodes,
searchEmojisByHexcodes, searchEmojisByHexcodes,
} from './database'; } from './database';
import { loadEmojiLocale } from './index';
import { import {
emojiToUnicodeHex, emojiToUnicodeHex,
twemojiHasBorder, twemojiHasBorder,
@ -34,18 +31,38 @@ import type {
LocaleOrCustom, LocaleOrCustom,
UnicodeEmojiToken, UnicodeEmojiToken,
} from './types'; } from './types';
import { stringHasUnicodeFlags } from './utils'; import {
anyEmojiRegex,
emojiLogger,
stringHasAnyEmoji,
stringHasUnicodeFlags,
} from './utils';
const localeCacheMap = new Map<LocaleOrCustom, EmojiStateMap>([ const log = emojiLogger('render');
[EMOJI_TYPE_CUSTOM, new Map()],
]);
// Emojifies an element. This modifies the element in place, replacing text nodes with emojified versions. /**
* Emojifies an element. This modifies the element in place, replacing text nodes with emojified versions.
*/
export async function emojifyElement<Element extends HTMLElement>( export async function emojifyElement<Element extends HTMLElement>(
element: Element, element: Element,
appState: EmojiAppState, appState: EmojiAppState,
extraEmojis: ExtraCustomEmojiMap = {}, extraEmojis: ExtraCustomEmojiMap = {},
): Promise<Element> { ): Promise<Element | null> {
const cacheKey = createCacheKey(element, appState, extraEmojis);
const cached = getCached(cacheKey);
if (cached !== undefined) {
log('Cache hit on %s', element.outerHTML);
if (cached === null) {
return null;
}
element.innerHTML = cached;
return element;
}
if (!stringHasAnyEmoji(element.innerHTML)) {
updateCache(cacheKey, null);
return null;
}
perf.start('emojifyElement()');
const queue: (HTMLElement | Text)[] = [element]; const queue: (HTMLElement | Text)[] = [element];
while (queue.length > 0) { while (queue.length > 0) {
const current = queue.shift(); const current = queue.shift();
@ -61,7 +78,7 @@ export async function emojifyElement<Element extends HTMLElement>(
current.textContent && current.textContent &&
(current instanceof Text || !current.hasChildNodes()) (current instanceof Text || !current.hasChildNodes())
) { ) {
const renderedContent = await emojifyText( const renderedContent = await textToElementArray(
current.textContent, current.textContent,
appState, appState,
extraEmojis, extraEmojis,
@ -70,7 +87,7 @@ export async function emojifyElement<Element extends HTMLElement>(
if (!(current instanceof Text)) { if (!(current instanceof Text)) {
current.textContent = null; // Clear the text content if it's not a Text node. current.textContent = null; // Clear the text content if it's not a Text node.
} }
current.replaceWith(renderedToHTMLFragment(renderedContent)); current.replaceWith(renderedToHTML(renderedContent));
} }
continue; continue;
} }
@ -81,6 +98,8 @@ export async function emojifyElement<Element extends HTMLElement>(
} }
} }
} }
updateCache(cacheKey, element.innerHTML);
perf.stop('emojifyElement()');
return element; return element;
} }
@ -88,7 +107,54 @@ export async function emojifyText(
text: string, text: string,
appState: EmojiAppState, appState: EmojiAppState,
extraEmojis: ExtraCustomEmojiMap = {}, extraEmojis: ExtraCustomEmojiMap = {},
): Promise<string | null> {
const cacheKey = createCacheKey(text, appState, extraEmojis);
const cached = getCached(cacheKey);
if (cached !== undefined) {
log('Cache hit on %s', text);
return cached ?? text;
}
if (!stringHasAnyEmoji(text)) {
updateCache(cacheKey, null);
return text;
}
const eleArray = await textToElementArray(text, appState, extraEmojis);
if (!eleArray) {
updateCache(cacheKey, null);
return text;
}
const rendered = renderedToHTML(eleArray, document.createElement('div'));
updateCache(cacheKey, rendered.innerHTML);
return rendered.innerHTML;
}
// Private functions
const {
set: updateCache,
get: getCached,
clear: cacheClear,
} = createLimitedCache<string | null>({ log: log.extend('cache') });
function createCacheKey(
input: HTMLElement | string,
appState: EmojiAppState,
extraEmojis: ExtraCustomEmojiMap,
) { ) {
return JSON.stringify([
input instanceof HTMLElement ? input.outerHTML : input,
appState,
extraEmojis,
]);
}
type EmojifiedTextArray = (string | HTMLImageElement)[];
async function textToElementArray(
text: string,
appState: EmojiAppState,
extraEmojis: ExtraCustomEmojiMap = {},
): Promise<EmojifiedTextArray | null> {
// Exit if no text to convert. // Exit if no text to convert.
if (!text.trim()) { if (!text.trim()) {
return null; return null;
@ -102,10 +168,9 @@ export async function emojifyText(
} }
// Get all emoji from the state map, loading any missing ones. // Get all emoji from the state map, loading any missing ones.
await ensureLocalesAreLoaded(appState.locales); await loadMissingEmojiIntoCache(tokens, appState, extraEmojis);
await loadMissingEmojiIntoCache(tokens, appState.locales);
const renderedFragments: (string | HTMLImageElement)[] = []; const renderedFragments: EmojifiedTextArray = [];
for (const token of tokens) { for (const token of tokens) {
if (typeof token !== 'string' && shouldRenderImage(token, appState.mode)) { if (typeof token !== 'string' && shouldRenderImage(token, appState.mode)) {
let state: EmojiState | undefined; let state: EmojiState | undefined;
@ -125,7 +190,7 @@ export async function emojifyText(
// If the state is valid, create an image element. Otherwise, just append as text. // If the state is valid, create an image element. Otherwise, just append as text.
if (state && typeof state !== 'string') { if (state && typeof state !== 'string') {
const image = stateToImage(state); const image = stateToImage(state, appState);
renderedFragments.push(image); renderedFragments.push(image);
continue; continue;
} }
@ -137,21 +202,6 @@ export async function emojifyText(
return renderedFragments; return renderedFragments;
} }
// Private functions
async function ensureLocalesAreLoaded(locales: Locale[]) {
const missingLocales = await findMissingLocales(locales);
for (const locale of missingLocales) {
await loadEmojiLocale(locale);
}
}
const CUSTOM_EMOJI_REGEX = /:([a-z0-9_]+):/i;
const TOKENIZE_REGEX = new RegExp(
`(${EMOJI_REGEX.source}|${CUSTOM_EMOJI_REGEX.source})`,
'g',
);
type TokenizedText = (string | EmojiToken)[]; type TokenizedText = (string | EmojiToken)[];
export function tokenizeText(text: string): TokenizedText { export function tokenizeText(text: string): TokenizedText {
@ -161,7 +211,7 @@ export function tokenizeText(text: string): TokenizedText {
const tokens = []; const tokens = [];
let lastIndex = 0; let lastIndex = 0;
for (const match of text.matchAll(TOKENIZE_REGEX)) { for (const match of text.matchAll(anyEmojiRegex())) {
if (match.index > lastIndex) { if (match.index > lastIndex) {
tokens.push(text.slice(lastIndex, match.index)); tokens.push(text.slice(lastIndex, match.index));
} }
@ -189,8 +239,18 @@ export function tokenizeText(text: string): TokenizedText {
return tokens; return tokens;
} }
const localeCacheMap = new Map<LocaleOrCustom, EmojiStateMap>([
[
EMOJI_TYPE_CUSTOM,
createLimitedCache<EmojiState>({ log: log.extend('custom') }),
],
]);
function cacheForLocale(locale: LocaleOrCustom): EmojiStateMap { function cacheForLocale(locale: LocaleOrCustom): EmojiStateMap {
return localeCacheMap.get(locale) ?? (new Map() as EmojiStateMap); return (
localeCacheMap.get(locale) ??
createLimitedCache<EmojiState>({ log: log.extend(locale) })
);
} }
function emojiForLocale( function emojiForLocale(
@ -203,7 +263,8 @@ function emojiForLocale(
async function loadMissingEmojiIntoCache( async function loadMissingEmojiIntoCache(
tokens: TokenizedText, tokens: TokenizedText,
locales: Locale[], { mode, currentLocale }: EmojiAppState,
extraEmojis: ExtraCustomEmojiMap,
) { ) {
const missingUnicodeEmoji = new Set<string>(); const missingUnicodeEmoji = new Set<string>();
const missingCustomEmoji = new Set<string>(); const missingCustomEmoji = new Set<string>();
@ -217,42 +278,41 @@ async function loadMissingEmojiIntoCache(
// If this is a custom emoji, check it separately. // If this is a custom emoji, check it separately.
if (token.type === EMOJI_TYPE_CUSTOM) { if (token.type === EMOJI_TYPE_CUSTOM) {
const code = token.code; const code = token.code;
if (code in extraEmojis) {
continue; // We don't care about extra emoji.
}
const emojiState = emojiForLocale(code, EMOJI_TYPE_CUSTOM); const emojiState = emojiForLocale(code, EMOJI_TYPE_CUSTOM);
if (!emojiState) { if (!emojiState) {
missingCustomEmoji.add(code); missingCustomEmoji.add(code);
} }
// Otherwise this is a unicode emoji, so check it against all locales. // Otherwise this is a unicode emoji, so check it against all locales.
} else { } else if (shouldRenderImage(token, mode)) {
const code = emojiToUnicodeHex(token.code); const code = emojiToUnicodeHex(token.code);
if (missingUnicodeEmoji.has(code)) { if (missingUnicodeEmoji.has(code)) {
continue; // Already marked as missing. continue; // Already marked as missing.
} }
for (const locale of locales) { const emojiState = emojiForLocale(code, currentLocale);
const emojiState = emojiForLocale(code, locale); if (!emojiState) {
if (!emojiState) { // If it's missing in one locale, we consider it missing for all.
// If it's missing in one locale, we consider it missing for all. missingUnicodeEmoji.add(code);
missingUnicodeEmoji.add(code);
}
} }
} }
} }
if (missingUnicodeEmoji.size > 0) { if (missingUnicodeEmoji.size > 0) {
const missingEmojis = Array.from(missingUnicodeEmoji).toSorted(); const missingEmojis = Array.from(missingUnicodeEmoji).toSorted();
for (const locale of locales) { const emojis = await searchEmojisByHexcodes(missingEmojis, currentLocale);
const emojis = await searchEmojisByHexcodes(missingEmojis, locale); const cache = cacheForLocale(currentLocale);
const cache = cacheForLocale(locale); for (const emoji of emojis) {
for (const emoji of emojis) { cache.set(emoji.hexcode, { type: EMOJI_TYPE_UNICODE, data: emoji });
cache.set(emoji.hexcode, { type: EMOJI_TYPE_UNICODE, data: emoji });
}
const notFoundEmojis = missingEmojis.filter((code) =>
emojis.every((emoji) => emoji.hexcode !== code),
);
for (const code of notFoundEmojis) {
cache.set(code, EMOJI_STATE_MISSING); // Mark as missing if not found, as it's probably not a valid emoji.
}
localeCacheMap.set(locale, cache);
} }
const notFoundEmojis = missingEmojis.filter((code) =>
emojis.every((emoji) => emoji.hexcode !== code),
);
for (const code of notFoundEmojis) {
cache.set(code, EMOJI_STATE_MISSING); // Mark as missing if not found, as it's probably not a valid emoji.
}
localeCacheMap.set(currentLocale, cache);
} }
if (missingCustomEmoji.size > 0) { if (missingCustomEmoji.size > 0) {
@ -288,22 +348,24 @@ function shouldRenderImage(token: EmojiToken, mode: EmojiMode): boolean {
return true; return true;
} }
function stateToImage(state: EmojiLoadedState) { function stateToImage(state: EmojiLoadedState, appState: EmojiAppState) {
const image = document.createElement('img'); const image = document.createElement('img');
image.draggable = false; image.draggable = false;
image.classList.add('emojione'); image.classList.add('emojione');
if (state.type === EMOJI_TYPE_UNICODE) { if (state.type === EMOJI_TYPE_UNICODE) {
const emojiInfo = twemojiHasBorder(unicodeToTwemojiHex(state.data.hexcode)); const emojiInfo = twemojiHasBorder(unicodeToTwemojiHex(state.data.hexcode));
if (emojiInfo.hasLightBorder) { let fileName = emojiInfo.hexCode;
image.dataset.lightCode = `${emojiInfo.hexCode}_BORDER`; if (
} else if (emojiInfo.hasDarkBorder) { (appState.darkTheme && emojiInfo.hasDarkBorder) ||
image.dataset.darkCode = `${emojiInfo.hexCode}_BORDER`; (!appState.darkTheme && emojiInfo.hasLightBorder)
) {
fileName = `${emojiInfo.hexCode}_border`;
} }
image.alt = state.data.unicode; image.alt = state.data.unicode;
image.title = state.data.label; image.title = state.data.label;
image.src = `${assetHost}/emoji/${emojiInfo.hexCode}.svg`; image.src = `${assetHost}/emoji/${fileName}.svg`;
} else { } else {
// Custom emoji // Custom emoji
const shortCode = `:${state.data.shortcode}:`; const shortCode = `:${state.data.shortcode}:`;
@ -318,8 +380,16 @@ function stateToImage(state: EmojiLoadedState) {
return image; return image;
} }
function renderedToHTMLFragment(renderedArray: (string | HTMLImageElement)[]) { function renderedToHTML(renderedArray: EmojifiedTextArray): DocumentFragment;
const fragment = document.createDocumentFragment(); function renderedToHTML<ParentType extends ParentNode>(
renderedArray: EmojifiedTextArray,
parent: ParentType,
): ParentType;
function renderedToHTML(
renderedArray: EmojifiedTextArray,
parent: ParentNode | null = null,
) {
const fragment = parent ?? document.createDocumentFragment();
for (const fragmentItem of renderedArray) { for (const fragmentItem of renderedArray) {
if (typeof fragmentItem === 'string') { if (typeof fragmentItem === 'string') {
fragment.appendChild(document.createTextNode(fragmentItem)); fragment.appendChild(document.createTextNode(fragmentItem));
@ -329,3 +399,9 @@ function renderedToHTMLFragment(renderedArray: (string | HTMLImageElement)[]) {
} }
return fragment; return fragment;
} }
// Testing helpers
export const testCacheClear = () => {
cacheClear();
localeCacheMap.clear();
};

View File

@ -1,6 +1,10 @@
import type { List as ImmutableList } from 'immutable';
import type { FlatCompactEmoji, Locale } from 'emojibase'; import type { FlatCompactEmoji, Locale } from 'emojibase';
import type { ApiCustomEmojiJSON } from '@/mastodon/api_types/custom_emoji'; import type { ApiCustomEmojiJSON } from '@/mastodon/api_types/custom_emoji';
import type { CustomEmoji } from '@/mastodon/models/custom_emoji';
import type { LimitedCache } from '@/mastodon/utils/cache';
import type { import type {
EMOJI_MODE_NATIVE, EMOJI_MODE_NATIVE,
@ -22,6 +26,7 @@ export interface EmojiAppState {
locales: Locale[]; locales: Locale[];
currentLocale: Locale; currentLocale: Locale;
mode: EmojiMode; mode: EmojiMode;
darkTheme: boolean;
} }
export interface UnicodeEmojiToken { export interface UnicodeEmojiToken {
@ -45,7 +50,7 @@ export interface EmojiStateUnicode {
} }
export interface EmojiStateCustom { export interface EmojiStateCustom {
type: typeof EMOJI_TYPE_CUSTOM; type: typeof EMOJI_TYPE_CUSTOM;
data: CustomEmojiData; data: CustomEmojiRenderFields;
} }
export type EmojiState = export type EmojiState =
| EmojiStateMissing | EmojiStateMissing
@ -53,9 +58,16 @@ export type EmojiState =
| EmojiStateCustom; | EmojiStateCustom;
export type EmojiLoadedState = EmojiStateUnicode | EmojiStateCustom; export type EmojiLoadedState = EmojiStateUnicode | EmojiStateCustom;
export type EmojiStateMap = Map<string, EmojiState>; export type EmojiStateMap = LimitedCache<string, EmojiState>;
export type ExtraCustomEmojiMap = Record<string, ApiCustomEmojiJSON>; export type CustomEmojiMapArg =
| ExtraCustomEmojiMap
| ImmutableList<CustomEmoji>;
export type CustomEmojiRenderFields = Pick<
CustomEmojiData,
'shortcode' | 'static_url' | 'url'
>;
export type ExtraCustomEmojiMap = Record<string, CustomEmojiRenderFields>;
export interface TwemojiBorderInfo { export interface TwemojiBorderInfo {
hexCode: string; hexCode: string;

View File

@ -1,8 +1,14 @@
import { stringHasUnicodeEmoji, stringHasUnicodeFlags } from './utils'; import {
stringHasAnyEmoji,
stringHasCustomEmoji,
stringHasUnicodeEmoji,
stringHasUnicodeFlags,
} from './utils';
describe('stringHasEmoji', () => { describe('stringHasUnicodeEmoji', () => {
test.concurrent.for([ test.concurrent.for([
['only text', false], ['only text', false],
['text with non-emoji symbols ™©', false],
['text with emoji 😀', true], ['text with emoji 😀', true],
['multiple emojis 😀😃😄', true], ['multiple emojis 😀😃😄', true],
['emoji with skin tone 👍🏽', true], ['emoji with skin tone 👍🏽', true],
@ -19,14 +25,14 @@ describe('stringHasEmoji', () => {
['emoji with enclosing keycap #️⃣', true], ['emoji with enclosing keycap #️⃣', true],
['emoji with no visible glyph \u200D', false], ['emoji with no visible glyph \u200D', false],
] as const)( ] as const)(
'stringHasEmoji has emojis in "%s": %o', 'stringHasUnicodeEmoji has emojis in "%s": %o',
([text, expected], { expect }) => { ([text, expected], { expect }) => {
expect(stringHasUnicodeEmoji(text)).toBe(expected); expect(stringHasUnicodeEmoji(text)).toBe(expected);
}, },
); );
}); });
describe('stringHasFlags', () => { describe('stringHasUnicodeFlags', () => {
test.concurrent.for([ test.concurrent.for([
['EU 🇪🇺', true], ['EU 🇪🇺', true],
['Germany 🇩🇪', true], ['Germany 🇩🇪', true],
@ -45,3 +51,27 @@ describe('stringHasFlags', () => {
}, },
); );
}); });
describe('stringHasCustomEmoji', () => {
test('string with custom emoji returns true', () => {
expect(stringHasCustomEmoji(':custom: :test:')).toBeTruthy();
});
test('string without custom emoji returns false', () => {
expect(stringHasCustomEmoji('🏳️‍🌈 :🏳️‍🌈: text ™')).toBeFalsy();
});
});
describe('stringHasAnyEmoji', () => {
test('string without any emoji or characters', () => {
expect(stringHasAnyEmoji('normal text. 12356?!')).toBeFalsy();
});
test('string with non-emoji characters', () => {
expect(stringHasAnyEmoji('™©')).toBeFalsy();
});
test('has unicode emoji', () => {
expect(stringHasAnyEmoji('🏳️‍🌈🔥🇸🇹 👩‍🔬')).toBeTruthy();
});
test('has custom emoji', () => {
expect(stringHasAnyEmoji(':test: :custom:')).toBeTruthy();
});
});

View File

@ -1,13 +1,56 @@
import EMOJI_REGEX from 'emojibase-regex/emoji-loose'; import debug from 'debug';
export function stringHasUnicodeEmoji(text: string): boolean { import { emojiRegexPolyfill } from '@/mastodon/polyfills';
return EMOJI_REGEX.test(text);
export function emojiLogger(segment: string) {
return debug(`emojis:${segment}`);
} }
// From https://github.com/talkjs/country-flag-emoji-polyfill/blob/master/src/index.ts#L49-L50 export function stringHasUnicodeEmoji(input: string): boolean {
const EMOJIS_FLAGS_REGEX = return new RegExp(EMOJI_REGEX, supportedFlags()).test(input);
/[\u{1F1E6}-\u{1F1FF}|\u{E0062}-\u{E0063}|\u{E0065}|\u{E0067}|\u{E006C}|\u{E006E}|\u{E0073}-\u{E0074}|\u{E0077}|\u{E007F}]+/u;
export function stringHasUnicodeFlags(text: string): boolean {
return EMOJIS_FLAGS_REGEX.test(text);
} }
export function stringHasUnicodeFlags(input: string): boolean {
if (supportsRegExpSets()) {
return new RegExp(
'\\p{RGI_Emoji_Flag_Sequence}|\\p{RGI_Emoji_Tag_Sequence}',
'v',
).test(input);
}
return new RegExp(
// First range is regional indicator symbols,
// Second is a black flag + 0-9|a-z tag chars + cancel tag.
// See: https://en.wikipedia.org/wiki/Regional_indicator_symbol
'(?:\uD83C[\uDDE6-\uDDFF]){2}|\uD83C\uDFF4(?:\uDB40[\uDC30-\uDC7A])+\uDB40\uDC7F',
).test(input);
}
// Constant as this is supported by all browsers.
const CUSTOM_EMOJI_REGEX = /:([a-z0-9_]+):/i;
export function stringHasCustomEmoji(input: string) {
return CUSTOM_EMOJI_REGEX.test(input);
}
export function stringHasAnyEmoji(input: string) {
return stringHasUnicodeEmoji(input) || stringHasCustomEmoji(input);
}
export function anyEmojiRegex() {
return new RegExp(
`${EMOJI_REGEX}|${CUSTOM_EMOJI_REGEX.source}`,
supportedFlags('gi'),
);
}
function supportsRegExpSets() {
return 'unicodeSets' in RegExp.prototype;
}
function supportedFlags(flags = '') {
if (supportsRegExpSets()) {
return `${flags}v`;
}
return flags;
}
const EMOJI_REGEX = emojiRegexPolyfill?.source ?? '\\p{RGI_Emoji}';

View File

@ -5,9 +5,14 @@ self.postMessage('ready'); // After the worker is ready, notify the main thread
function handleMessage(event: MessageEvent<string>) { function handleMessage(event: MessageEvent<string>) {
const { data: locale } = event; const { data: locale } = event;
if (locale !== 'custom') { void loadData(locale);
void importEmojiData(locale); }
} else {
void importCustomEmojiData(); async function loadData(locale: string) {
} if (locale !== 'custom') {
await importEmojiData(locale);
} else {
await importCustomEmojiData();
}
self.postMessage(`loaded ${locale}`);
} }

View File

@ -143,6 +143,17 @@ class ColumnSettings extends PureComponent {
</div> </div>
</section> </section>
<section role='group' aria-labelledby='notifications-quote'>
<h3 id='notifications-quote'><FormattedMessage id='notifications.column_settings.quote' defaultMessage='Quotes:' /></h3>
<div className='column-settings__row'>
<SettingToggle disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'quote']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'quote']} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'quote']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'quote']} onChange={onChange} label={soundStr} />
</div>
</section>
<section role='group' aria-labelledby='notifications-poll'> <section role='group' aria-labelledby='notifications-poll'>
<h3 id='notifications-poll'><FormattedMessage id='notifications.column_settings.poll' defaultMessage='Poll results:' /></h3> <h3 id='notifications-poll'><FormattedMessage id='notifications.column_settings.poll' defaultMessage='Poll results:' /></h3>

View File

@ -8,9 +8,9 @@ import { Link, withRouter } from 'react-router-dom';
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 EditIcon from '@/material-icons/400-24px/edit.svg?react'; import EditIcon from '@/material-icons/400-24px/edit.svg?react';
import FlagIcon from '@/material-icons/400-24px/flag-fill.svg?react'; import FlagIcon from '@/material-icons/400-24px/flag-fill.svg?react';
import FormatQuoteIcon from '@/material-icons/400-24px/format_quote.svg?react';
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react'; import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react'; import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react';
import PersonIcon from '@/material-icons/400-24px/person-fill.svg?react'; import PersonIcon from '@/material-icons/400-24px/person-fill.svg?react';
@ -42,6 +42,7 @@ const messages = defineMessages({
adminReport: { id: 'notification.admin.report', defaultMessage: '{name} reported {target}' }, adminReport: { id: 'notification.admin.report', defaultMessage: '{name} reported {target}' },
relationshipsSevered: { id: 'notification.relationships_severance_event', defaultMessage: 'Lost connections with {name}' }, relationshipsSevered: { id: 'notification.relationships_severance_event', defaultMessage: 'Lost connections with {name}' },
moderationWarning: { id: 'notification.moderation_warning', defaultMessage: 'You have received a moderation warning' }, moderationWarning: { id: 'notification.moderation_warning', defaultMessage: 'You have received a moderation warning' },
quote: { id: 'notification.label.quote', defaultMessage: '{name} quoted your post'}
}); });
const notificationForScreenReader = (intl, message, timestamp) => { const notificationForScreenReader = (intl, message, timestamp) => {
@ -251,6 +252,36 @@ class Notification extends ImmutablePureComponent {
); );
} }
renderQuote (notification, link) {
const { intl, unread } = this.props;
return (
<Hotkeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-quote focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.quote, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
<div className='notification__message'>
<Icon id='quote' icon={FormatQuoteIcon} />
<span title={notification.get('created_at')}>
<FormattedMessage id='notification.label.quote' defaultMessage='{name} quoted your post' values={{ name: link }} />
</span>
</div>
<StatusQuoteManager
id={notification.get('status')}
account={notification.get('account')}
muted
withDismiss
hidden={this.props.hidden}
getScrollPosition={this.props.getScrollPosition}
updateScrollBottom={this.props.updateScrollBottom}
cachedMediaWidth={this.props.cachedMediaWidth}
cacheMediaWidth={this.props.cacheMediaWidth}
/>
</div>
</Hotkeys>
);
}
renderStatus (notification, link) { renderStatus (notification, link) {
const { intl, unread, status } = this.props; const { intl, unread, status } = this.props;
@ -467,6 +498,8 @@ class Notification extends ImmutablePureComponent {
return this.renderFollowRequest(notification, account, link); return this.renderFollowRequest(notification, account, link);
case 'mention': case 'mention':
return this.renderMention(notification); return this.renderMention(notification);
case 'quote':
return this.renderQuote(notification);
case 'favourite': case 'favourite':
return this.renderFavourite(notification, link); return this.renderFavourite(notification, link);
case 'reblog': case 'reblog':

View File

@ -15,6 +15,7 @@ import { NotificationFollowRequest } from './notification_follow_request';
import { NotificationMention } from './notification_mention'; import { NotificationMention } from './notification_mention';
import { NotificationModerationWarning } from './notification_moderation_warning'; import { NotificationModerationWarning } from './notification_moderation_warning';
import { NotificationPoll } from './notification_poll'; import { NotificationPoll } from './notification_poll';
import { NotificationQuote } from './notification_quote';
import { NotificationReblog } from './notification_reblog'; import { NotificationReblog } from './notification_reblog';
import { NotificationSeveredRelationships } from './notification_severed_relationships'; import { NotificationSeveredRelationships } from './notification_severed_relationships';
import { NotificationStatus } from './notification_status'; import { NotificationStatus } from './notification_status';
@ -91,6 +92,11 @@ export const NotificationGroup: React.FC<{
<NotificationMention unread={unread} notification={notificationGroup} /> <NotificationMention unread={unread} notification={notificationGroup} />
); );
break; break;
case 'quote':
content = (
<NotificationQuote unread={unread} notification={notificationGroup} />
);
break;
case 'follow': case 'follow':
content = ( content = (
<NotificationFollow unread={unread} notification={notificationGroup} /> <NotificationFollow unread={unread} notification={notificationGroup} />

View File

@ -0,0 +1,33 @@
import { FormattedMessage } from 'react-intl';
import FormatQuoteIcon from '@/material-icons/400-24px/format_quote.svg?react';
import type { NotificationGroupQuote } from 'mastodon/models/notification_group';
import type { LabelRenderer } from './notification_group_with_status';
import { NotificationWithStatus } from './notification_with_status';
const quoteLabelRenderer: LabelRenderer = (displayName) => (
<FormattedMessage
id='notification.label.quote'
defaultMessage='{name} quoted your post'
values={{ name: displayName }}
/>
);
export const NotificationQuote: React.FC<{
notification: NotificationGroupQuote;
unread: boolean;
}> = ({ notification, unread }) => {
return (
<NotificationWithStatus
type='quote'
icon={FormatQuoteIcon}
iconId='quote'
accountIds={notification.sampleAccountIds}
count={notification.notifications_count}
statusId={notification.statusId}
labelRenderer={quoteLabelRenderer}
unread={unread}
/>
);
};

View File

@ -21,6 +21,7 @@ import { openModal } from 'mastodon/actions/modal';
import { IconButton } from 'mastodon/components/icon_button'; import { IconButton } from 'mastodon/components/icon_button';
import { useIdentity } from 'mastodon/identity_context'; import { useIdentity } from 'mastodon/identity_context';
import { me } from 'mastodon/initial_state'; import { me } from 'mastodon/initial_state';
import type { Account } from 'mastodon/models/account';
import type { Status } from 'mastodon/models/status'; import type { Status } from 'mastodon/models/status';
import { makeGetStatus } from 'mastodon/selectors'; import { makeGetStatus } from 'mastodon/selectors';
import type { RootState } from 'mastodon/store'; import type { RootState } from 'mastodon/store';
@ -66,10 +67,7 @@ export const Footer: React.FC<{
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const getStatus = useMemo(() => makeGetStatus(), []) as GetStatusSelector; const getStatus = useMemo(() => makeGetStatus(), []) as GetStatusSelector;
const status = useAppSelector((state) => getStatus(state, { id: statusId })); const status = useAppSelector((state) => getStatus(state, { id: statusId }));
const accountId = status?.get('account') as string | undefined; const account = status?.get('account') as Account | undefined;
const account = useAppSelector((state) =>
accountId ? state.accounts.get(accountId) : undefined,
);
const askReplyConfirmation = useAppSelector( const askReplyConfirmation = useAppSelector(
(state) => (state.compose.get('text') as string).trim().length !== 0, (state) => (state.compose.get('text') as string).trim().length !== 0,
); );

View File

@ -61,22 +61,29 @@ const messages = defineMessages({
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' }, openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
revokeQuote: { id: 'status.revoke_quote', defaultMessage: 'Remove my post from @{name}s post' },
}); });
const mapStateToProps = (state, { status }) => ({ const mapStateToProps = (state, { status }) => {
relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]), const quotedStatusId = status.getIn(['quote', 'quoted_status']);
}); return ({
relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]),
quotedAccountId: quotedStatusId ? state.getIn(['statuses', quotedStatusId, 'account']) : null,
});
};
class ActionBar extends PureComponent { class ActionBar extends PureComponent {
static propTypes = { static propTypes = {
identity: identityContextPropShape, identity: identityContextPropShape,
status: ImmutablePropTypes.map.isRequired, status: ImmutablePropTypes.map.isRequired,
relationship: ImmutablePropTypes.record, relationship: ImmutablePropTypes.record,
quotedAccountId: ImmutablePropTypes.string,
onReply: PropTypes.func.isRequired, onReply: PropTypes.func.isRequired,
onReblog: PropTypes.func.isRequired, onReblog: PropTypes.func.isRequired,
onFavourite: PropTypes.func.isRequired, onFavourite: PropTypes.func.isRequired,
onBookmark: PropTypes.func.isRequired, onBookmark: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired,
onRevokeQuote: PropTypes.func,
onEdit: PropTypes.func.isRequired, onEdit: PropTypes.func.isRequired,
onDirect: PropTypes.func.isRequired, onDirect: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired,
@ -113,6 +120,10 @@ class ActionBar extends PureComponent {
this.props.onDelete(this.props.status); this.props.onDelete(this.props.status);
}; };
handleRevokeQuoteClick = () => {
this.props.onRevokeQuote(this.props.status);
}
handleRedraftClick = () => { handleRedraftClick = () => {
this.props.onDelete(this.props.status, true); this.props.onDelete(this.props.status, true);
}; };
@ -193,7 +204,7 @@ class ActionBar extends PureComponent {
}; };
render () { render () {
const { status, relationship, intl } = this.props; const { status, relationship, quotedAccountId, intl } = this.props;
const { signedIn, permissions } = this.props.identity; const { signedIn, permissions } = this.props.identity;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
@ -237,6 +248,10 @@ class ActionBar extends PureComponent {
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
menu.push(null); menu.push(null);
if (quotedAccountId === me) {
menu.push({ text: intl.formatMessage(messages.revokeQuote, { name: account.get('username') }), action: this.handleRevokeQuoteClick, dangerous: true });
}
if (relationship && relationship.get('muting')) { if (relationship && relationship.get('muting')) {
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick }); menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick });
} else { } else {

View File

@ -2,8 +2,6 @@ import { useEffect, useState, useCallback } from 'react';
import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { import {
fetchContext, fetchContext,
completeContextRefresh, completeContextRefresh,
@ -22,11 +20,15 @@ const messages = defineMessages({
export const RefreshController: React.FC<{ export const RefreshController: React.FC<{
statusId: string; statusId: string;
withBorder?: boolean; }> = ({ statusId }) => {
}> = ({ statusId, withBorder }) => {
const refresh = useAppSelector( const refresh = useAppSelector(
(state) => state.contexts.refreshing[statusId], (state) => state.contexts.refreshing[statusId],
); );
const autoRefresh = useAppSelector(
(state) =>
!state.contexts.replies[statusId] ||
state.contexts.replies[statusId].length === 0,
);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const intl = useIntl(); const intl = useIntl();
const [ready, setReady] = useState(false); const [ready, setReady] = useState(false);
@ -42,6 +44,11 @@ export const RefreshController: React.FC<{
dispatch(completeContextRefresh({ statusId })); dispatch(completeContextRefresh({ statusId }));
if (result.async_refresh.result_count > 0) { if (result.async_refresh.result_count > 0) {
if (autoRefresh) {
void dispatch(fetchContext({ statusId }));
return '';
}
setReady(true); setReady(true);
} }
} else { } else {
@ -60,7 +67,7 @@ export const RefreshController: React.FC<{
return () => { return () => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
}; };
}, [dispatch, setReady, statusId, refresh]); }, [dispatch, setReady, statusId, refresh, autoRefresh]);
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
setLoading(true); setLoading(true);
@ -78,12 +85,7 @@ export const RefreshController: React.FC<{
if (ready && !loading) { if (ready && !loading) {
return ( return (
<button <button className='load-more load-gap' onClick={handleClick}>
className={classNames('load-more load-gap', {
'timeline-hint--with-descendants': withBorder,
})}
onClick={handleClick}
>
<FormattedMessage <FormattedMessage
id='status.context.load_new_replies' id='status.context.load_new_replies'
defaultMessage='New replies available' defaultMessage='New replies available'
@ -98,9 +100,7 @@ export const RefreshController: React.FC<{
return ( return (
<div <div
className={classNames('load-more load-gap', { className='load-more load-gap'
'timeline-hint--with-descendants': withBorder,
})}
aria-busy aria-busy
aria-live='polite' aria-live='polite'
aria-label={intl.formatMessage(messages.loading)} aria-label={intl.formatMessage(messages.loading)}

View File

@ -259,6 +259,12 @@ class Status extends ImmutablePureComponent {
} }
}; };
handleRevokeQuoteClick = (status) => {
const { dispatch } = this.props;
dispatch(openModal({ modalType: 'CONFIRM_REVOKE_QUOTE', modalProps: { statusId: status.get('id'), quotedStatusId: status.getIn(['quote', 'quoted_status']) }}));
};
handleEditClick = (status) => { handleEditClick = (status) => {
const { dispatch, askReplyConfirmation } = this.props; const { dispatch, askReplyConfirmation } = this.props;
@ -580,7 +586,6 @@ class Status extends ImmutablePureComponent {
remoteHint = ( remoteHint = (
<RefreshController <RefreshController
statusId={status.get('id')} statusId={status.get('id')}
withBorder={!!descendants}
/> />
); );
} }
@ -636,6 +641,7 @@ class Status extends ImmutablePureComponent {
onReblog={this.handleReblogClick} onReblog={this.handleReblogClick}
onBookmark={this.handleBookmarkClick} onBookmark={this.handleBookmarkClick}
onDelete={this.handleDeleteClick} onDelete={this.handleDeleteClick}
onRevokeQuote={this.handleRevokeQuoteClick}
onEdit={this.handleEditClick} onEdit={this.handleEditClick}
onDirect={this.handleDirectClick} onDirect={this.handleDirectClick}
onMention={this.handleMentionClick} onMention={this.handleMentionClick}
@ -653,8 +659,8 @@ class Status extends ImmutablePureComponent {
</div> </div>
</Hotkeys> </Hotkeys>
{descendants}
{remoteHint} {remoteHint}
{descendants}
</div> </div>
</ScrollContainer> </ScrollContainer>

View File

@ -10,3 +10,4 @@ export { ConfirmClearNotificationsModal } from './clear_notifications';
export { ConfirmLogOutModal } from './log_out'; export { ConfirmLogOutModal } from './log_out';
export { ConfirmFollowToListModal } from './follow_to_list'; export { ConfirmFollowToListModal } from './follow_to_list';
export { ConfirmMissingAltTextModal } from './missing_alt_text'; export { ConfirmMissingAltTextModal } from './missing_alt_text';
export { ConfirmRevokeQuoteModal } from './revoke_quote';

View File

@ -0,0 +1,48 @@
import { useCallback } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { revokeQuote } from 'mastodon/actions/interactions_typed';
import { useAppDispatch } from 'mastodon/store';
import type { BaseConfirmationModalProps } from './confirmation_modal';
import { ConfirmationModal } from './confirmation_modal';
const messages = defineMessages({
revokeQuoteTitle: {
id: 'confirmations.revoke_quote.title',
defaultMessage: 'Remove post?',
},
revokeQuoteMessage: {
id: 'confirmations.revoke_quote.message',
defaultMessage: 'This action cannot be undone.',
},
revokeQuoteConfirm: {
id: 'confirmations.revoke_quote.confirm',
defaultMessage: 'Remove post',
},
});
export const ConfirmRevokeQuoteModal: React.FC<
{
statusId: string;
quotedStatusId: string;
} & BaseConfirmationModalProps
> = ({ statusId, quotedStatusId, onClose }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const onConfirm = useCallback(() => {
void dispatch(revokeQuote({ quotedStatusId, statusId }));
}, [dispatch, statusId, quotedStatusId]);
return (
<ConfirmationModal
title={intl.formatMessage(messages.revokeQuoteTitle)}
message={intl.formatMessage(messages.revokeQuoteMessage)}
confirm={intl.formatMessage(messages.revokeQuoteConfirm)}
onConfirm={onConfirm}
onClose={onClose}
/>
);
};

View File

@ -37,6 +37,7 @@ import {
ConfirmLogOutModal, ConfirmLogOutModal,
ConfirmFollowToListModal, ConfirmFollowToListModal,
ConfirmMissingAltTextModal, ConfirmMissingAltTextModal,
ConfirmRevokeQuoteModal,
} from './confirmation_modals'; } from './confirmation_modals';
import { ImageModal } from './image_modal'; import { ImageModal } from './image_modal';
import MediaModal from './media_modal'; import MediaModal from './media_modal';
@ -59,6 +60,7 @@ export const MODAL_COMPONENTS = {
'CONFIRM_LOG_OUT': () => Promise.resolve({ default: ConfirmLogOutModal }), 'CONFIRM_LOG_OUT': () => Promise.resolve({ default: ConfirmLogOutModal }),
'CONFIRM_FOLLOW_TO_LIST': () => Promise.resolve({ default: ConfirmFollowToListModal }), 'CONFIRM_FOLLOW_TO_LIST': () => Promise.resolve({ default: ConfirmFollowToListModal }),
'CONFIRM_MISSING_ALT_TEXT': () => Promise.resolve({ default: ConfirmMissingAltTextModal }), 'CONFIRM_MISSING_ALT_TEXT': () => Promise.resolve({ default: ConfirmMissingAltTextModal }),
'CONFIRM_REVOKE_QUOTE': () => Promise.resolve({ default: ConfirmRevokeQuoteModal }),
'MUTE': MuteModal, 'MUTE': MuteModal,
'BLOCK': BlockModal, 'BLOCK': BlockModal,
'DOMAIN_BLOCK': DomainBlockModal, 'DOMAIN_BLOCK': DomainBlockModal,

View File

@ -306,10 +306,8 @@ export const ZoomableImage: React.FC<ZoomableImageProps> = ({
<animated.img <animated.img
style={{ transform }} style={{ transform }}
role='presentation'
ref={imageRef} ref={imageRef}
alt={alt} alt={alt}
title={alt}
lang={lang} lang={lang}
src={src} src={src}
width={width} width={width}

View File

@ -142,12 +142,4 @@ export function getAccessToken() {
return getMeta('access_token'); return getMeta('access_token');
} }
/**
* @param {string} feature
* @returns {boolean}
*/
export function isFeatureEnabled(feature) {
return initialState?.features?.includes(feature) || false;
}
export default initialState; export default initialState;

View File

@ -110,7 +110,7 @@
"announcement.announcement": "إعلان", "announcement.announcement": "إعلان",
"annual_report.summary.archetype.booster": "The cool-hunter", "annual_report.summary.archetype.booster": "The cool-hunter",
"annual_report.summary.archetype.lurker": "المتصفح الصامت", "annual_report.summary.archetype.lurker": "المتصفح الصامت",
"annual_report.summary.archetype.oracle": "حكيم", "annual_report.summary.archetype.oracle": "الحكيم",
"annual_report.summary.archetype.pollster": "مستطلع للرأي", "annual_report.summary.archetype.pollster": "مستطلع للرأي",
"annual_report.summary.archetype.replier": "الفراشة الاجتماعية", "annual_report.summary.archetype.replier": "الفراشة الاجتماعية",
"annual_report.summary.followers.followers": "المُتابِعُون", "annual_report.summary.followers.followers": "المُتابِعُون",
@ -424,8 +424,6 @@
"hints.profiles.see_more_followers": "عرض المزيد من المتابعين على {domain}", "hints.profiles.see_more_followers": "عرض المزيد من المتابعين على {domain}",
"hints.profiles.see_more_follows": "اطلع على المزيد من المتابعين على {domain}", "hints.profiles.see_more_follows": "اطلع على المزيد من المتابعين على {domain}",
"hints.profiles.see_more_posts": "عرض المزيد من المنشورات من {domain}", "hints.profiles.see_more_posts": "عرض المزيد من المنشورات من {domain}",
"hints.threads.replies_may_be_missing": "قد تكون الردود الواردة من الخوادم الأخرى غائبة.",
"hints.threads.see_more": "اطلع على المزيد من الردود على {domain}",
"home.column_settings.show_quotes": "إظهار الاقتباسات", "home.column_settings.show_quotes": "إظهار الاقتباسات",
"home.column_settings.show_reblogs": "اعرض المعاد نشرها", "home.column_settings.show_reblogs": "اعرض المعاد نشرها",
"home.column_settings.show_replies": "اعرض الردود", "home.column_settings.show_replies": "اعرض الردود",
@ -847,6 +845,7 @@
"status.bookmark": "أضفه إلى الفواصل المرجعية", "status.bookmark": "أضفه إلى الفواصل المرجعية",
"status.cancel_reblog_private": "إلغاء إعادة النشر", "status.cancel_reblog_private": "إلغاء إعادة النشر",
"status.cannot_reblog": "لا يمكن إعادة نشر هذا المنشور", "status.cannot_reblog": "لا يمكن إعادة نشر هذا المنشور",
"status.context.load_new_replies": "الردود الجديدة المتاحة",
"status.continued_thread": "تكملة للخيط", "status.continued_thread": "تكملة للخيط",
"status.copy": "انسخ رابط الرسالة", "status.copy": "انسخ رابط الرسالة",
"status.delete": "احذف", "status.delete": "احذف",
@ -873,12 +872,6 @@
"status.open": "وسّع هذا المنشور", "status.open": "وسّع هذا المنشور",
"status.pin": "دبّسه على الصفحة التعريفية", "status.pin": "دبّسه على الصفحة التعريفية",
"status.quote_error.filtered": "مُخفي بسبب إحدى إعدادات التصفية خاصتك", "status.quote_error.filtered": "مُخفي بسبب إحدى إعدادات التصفية خاصتك",
"status.quote_error.not_found": "لا يمكن عرض هذا المنشور.",
"status.quote_error.pending_approval": "هذا المنشور ينتظر موافقة صاحب المنشور الأصلي.",
"status.quote_error.rejected": "لا يمكن عرض هذا المنشور لأن صاحب المنشور الأصلي لا يسمح له بأن يكون مقتبس.",
"status.quote_error.removed": "تمت إزالة المنشور من قبل صاحبه.",
"status.quote_error.unauthorized": "لا يمكن عرض هذا المنشور لأنك لست مخولاً برؤيته.",
"status.quote_post_author": "منشور من {name}",
"status.read_more": "اقرأ المزيد", "status.read_more": "اقرأ المزيد",
"status.reblog": "إعادة النشر", "status.reblog": "إعادة النشر",
"status.reblog_private": "إعادة النشر إلى الجمهور الأصلي", "status.reblog_private": "إعادة النشر إلى الجمهور الأصلي",

View File

@ -266,7 +266,6 @@
"hashtag.counter_by_accounts": "{count, plural, one {{counter} participante} other {{counter} participantes}}", "hashtag.counter_by_accounts": "{count, plural, one {{counter} participante} other {{counter} participantes}}",
"hashtag.follow": "Siguir a la etiqueta", "hashtag.follow": "Siguir a la etiqueta",
"hashtag.unfollow": "Dexar de siguir a la etiqueta", "hashtag.unfollow": "Dexar de siguir a la etiqueta",
"hints.threads.replies_may_be_missing": "Ye posible que falten les rempuestes d'otros sirvidores.",
"home.column_settings.show_reblogs": "Amosar los artículos compartíos", "home.column_settings.show_reblogs": "Amosar los artículos compartíos",
"home.column_settings.show_replies": "Amosar les rempuestes", "home.column_settings.show_replies": "Amosar les rempuestes",
"home.pending_critical_update.body": "¡Anueva'l sirvidor de Mastodon namás que puedas!", "home.pending_critical_update.body": "¡Anueva'l sirvidor de Mastodon namás que puedas!",

View File

@ -1,13 +1,15 @@
{ {
"about.blocks": "Moderasiya olunan serverlər", "about.blocks": "Moderasiya edilmiş serverlər",
"about.contact": "Əlaqə:", "about.contact": "Əlaqə:",
"about.disclaimer": "Mastodon pulsuz, açıq-mənbəli proqram təminatıdır və Mastodon gGmbH-nin əmtəə nişanıdır.", "about.default_locale": "İlkin",
"about.domain_blocks.no_reason_available": "Səbəb naməlumdur", "about.disclaimer": "Mastodon ödənişsiz, açıq-mənbəli yazılımdır və Mastodon gGmbH-nin əmtəə nişanıdır.",
"about.domain_blocks.preamble": "Mastodon adətən fediversedəki hər hansısa bir serverdən olan məzmuna baxmaq və istifadəçilərlə qarşılıqlı əlaqədə olmaq imkanı verir. Bunlar bu serverdə edilmiş istisnalardır.", "about.domain_blocks.no_reason_available": "Səbəb mövcud deyil",
"about.domain_blocks.preamble": "Mastodon, adətən fediverse-dəki hər hansısa bir serverdən məzmuna baxmağınıza və istifadəçilərlə qarşılıqlı əlaqədə olmağınıza imkanı verir. Bunlar, bu serverdə edilmiş istisnalardır.",
"about.domain_blocks.silenced.explanation": "Siz bu serverdəki profilləri və məzmunu xüsusi olaraq axtarmasanız və ya izləməsəniz ümumiyyətlə görməyəcəksiniz.", "about.domain_blocks.silenced.explanation": "Siz bu serverdəki profilləri və məzmunu xüsusi olaraq axtarmasanız və ya izləməsəniz ümumiyyətlə görməyəcəksiniz.",
"about.domain_blocks.silenced.title": "Məhdudlaşdırılmış", "about.domain_blocks.silenced.title": "Məhdudlaşdırılmış",
"about.domain_blocks.suspended.explanation": "Bu serverdən heç bir data emal edilməyəcək, saxlanılmayacaq və ya mübadilə edilməyəcək və bu serverdən olan istifadəçilərlə hər hansı qarşılıqlı əlaqə qeyri-mümkün olacaq.", "about.domain_blocks.suspended.explanation": "Bu serverdəki heç bir veri emal edilməyəcək, saxlanılmayacaq və ya mübadilə edilməyəcək, bu serverdəki istifadəçilərlə hər hansısa bir qarşılıqlı əlaqə və ya ünsiyyət mümkünsüz olacaq.",
"about.domain_blocks.suspended.title": "Qadağa qoyulub", "about.domain_blocks.suspended.title": "Qadağa qoyulub",
"about.language_label": "Dil",
"about.not_available": "Bu məlumat bu serverdə əlçatan edilməyib.", "about.not_available": "Bu məlumat bu serverdə əlçatan edilməyib.",
"about.powered_by": "{mastodon} tərəfindən təchiz edilən desentralizasiya edilmiş sosial media", "about.powered_by": "{mastodon} tərəfindən təchiz edilən desentralizasiya edilmiş sosial media",
"about.rules": "Server qaydaları", "about.rules": "Server qaydaları",
@ -19,6 +21,7 @@
"account.block_domain": "{domain} domenini blokla", "account.block_domain": "{domain} domenini blokla",
"account.block_short": "Blok", "account.block_short": "Blok",
"account.blocked": "Bloklanıb", "account.blocked": "Bloklanıb",
"account.blocking": "Əngəlləmə",
"account.cancel_follow_request": "İzləməni ləğv et", "account.cancel_follow_request": "İzləməni ləğv et",
"account.copy": "Profil linkini kopyala", "account.copy": "Profil linkini kopyala",
"account.direct": "@{name} istifadəçisini fərdi olaraq etiketlə", "account.direct": "@{name} istifadəçisini fərdi olaraq etiketlə",
@ -27,6 +30,11 @@
"account.edit_profile": "Profili redaktə et", "account.edit_profile": "Profili redaktə et",
"account.enable_notifications": "@{name} paylaşım edəndə mənə bildiriş göndər", "account.enable_notifications": "@{name} paylaşım edəndə mənə bildiriş göndər",
"account.endorse": "Profildə seçilmişlərə əlavə et", "account.endorse": "Profildə seçilmişlərə əlavə et",
"account.familiar_followers_many": "{name1}, {name2} və tanıdığınız digər {othersCount, plural, one {digər bir nəfər} other {# nəfər}} izləyir",
"account.familiar_followers_one": "{name1} izləyir",
"account.familiar_followers_two": "{name1} və {name2} izləyir",
"account.featured": "Seçilmiş",
"account.featured.accounts": "Profillər",
"account.featured.hashtags": "Etiketler", "account.featured.hashtags": "Etiketler",
"account.featured_tags.last_status_at": "Son paylaşım {date} tarixində olub", "account.featured_tags.last_status_at": "Son paylaşım {date} tarixində olub",
"account.featured_tags.last_status_never": "Paylaşım yoxdur", "account.featured_tags.last_status_never": "Paylaşım yoxdur",
@ -35,9 +43,11 @@
"account.followers": "İzləyicilər", "account.followers": "İzləyicilər",
"account.followers.empty": "Bu istifadəçini hələ ki, heç kim izləmir.", "account.followers.empty": "Bu istifadəçini hələ ki, heç kim izləmir.",
"account.followers_counter": "{count, plural, one {{counter} izləyici} other {{counter} izləyici}}", "account.followers_counter": "{count, plural, one {{counter} izləyici} other {{counter} izləyici}}",
"account.followers_you_know_counter": "bildiyiniz {counter}",
"account.following": "İzləyir", "account.following": "İzləyir",
"account.following_counter": "{count, plural, one {{counter} izləyir} other {{counter} izləyir}}", "account.following_counter": "{count, plural, one {{counter} izləyir} other {{counter} izləyir}}",
"account.follows.empty": "Bu istifadəçi hələ ki, heç kimi izləmir.", "account.follows.empty": "Bu istifadəçi hələ ki, heç kimi izləmir.",
"account.follows_you": "Sizi izləyir",
"account.go_to_profile": "Profilə get", "account.go_to_profile": "Profilə get",
"account.hide_reblogs": "@{name} istifadəçisindən olan gücləndirmələri gizlət", "account.hide_reblogs": "@{name} istifadəçisindən olan gücləndirmələri gizlət",
"account.in_memoriam": "Xatirə.", "account.in_memoriam": "Xatirə.",
@ -52,18 +62,23 @@
"account.mute_notifications_short": "Bildirişləri səssizləşdir", "account.mute_notifications_short": "Bildirişləri səssizləşdir",
"account.mute_short": "Səssizləşdir", "account.mute_short": "Səssizləşdir",
"account.muted": "Səssizləşdirilib", "account.muted": "Səssizləşdirilib",
"account.muting": "Səssizə alınır",
"account.mutual": "Bir-birinizi izləyirsiniz",
"account.no_bio": "Təsvir göstərilməyib.", "account.no_bio": "Təsvir göstərilməyib.",
"account.open_original_page": "Orijinal səhifəni aç", "account.open_original_page": "Orijinal səhifəni aç",
"account.posts": "Paylaşım", "account.posts": "Paylaşım",
"account.posts_with_replies": "Paylaşım və cavablar", "account.posts_with_replies": "Paylaşım və cavablar",
"account.remove_from_followers": "{name} - izləyicilərdən çıxart",
"account.report": "@{name} istifadəçisini şikayət et", "account.report": "@{name} istifadəçisini şikayət et",
"account.requested": "Təsdiq edilməsi gözlənilir. İzləmə sorğusunu ləğv etmək üçün kliklə", "account.requested": "Təsdiq edilməsi gözlənilir. İzləmə sorğusunu ləğv etmək üçün kliklə",
"account.requested_follow": "{name} sizi izləmək sorğusu göndərib", "account.requested_follow": "{name} sizi izləmək sorğusu göndərib",
"account.requests_to_follow_you": "Sizi izləmək istəyir",
"account.share": "@{name} profilini paylaş", "account.share": "@{name} profilini paylaş",
"account.show_reblogs": "@{name} istifadəçisindən olan gücləndirmələri göstər", "account.show_reblogs": "@{name} istifadəçisindən olan gücləndirmələri göstər",
"account.statuses_counter": "{count, plural, one {{counter} paylaşım} other {{counter} paylaşım}}", "account.statuses_counter": "{count, plural, one {{counter} paylaşım} other {{counter} paylaşım}}",
"account.unblock": "@{name} blokunu aç", "account.unblock": "@{name} blokunu aç",
"account.unblock_domain": "{domain} domeninin blokunu aç", "account.unblock_domain": "{domain} domeninin blokunu aç",
"account.unblock_domain_short": "Əngəldən çıxart",
"account.unblock_short": "Bloku aç", "account.unblock_short": "Bloku aç",
"account.unendorse": "Profildə seçilmişlərə əlavə etmə", "account.unendorse": "Profildə seçilmişlərə əlavə etmə",
"account.unfollow": "İzləmədən çıxar", "account.unfollow": "İzləmədən çıxar",
@ -162,11 +177,11 @@
"column.pins": "Bərkidilmiş paylaşımlar", "column.pins": "Bərkidilmiş paylaşımlar",
"column.public": "Federasiya zaman qrafiki", "column.public": "Federasiya zaman qrafiki",
"column_back_button.label": "Geriyə", "column_back_button.label": "Geriyə",
"column_header.hide_settings": "Parametrləri gizlət", "column_header.hide_settings": "Ayarları gizlət",
"column_header.moveLeft_settings": "Sütunu sola köçür", "column_header.moveLeft_settings": "Sütunu sola köçür",
"column_header.moveRight_settings": "Sütunu sağa köçür", "column_header.moveRight_settings": "Sütunu sağa köçür",
"column_header.pin": "Bərkit", "column_header.pin": "Bərkit",
"column_header.show_settings": "Parametrləri göstər", "column_header.show_settings": "Ayarları göstər",
"column_header.unpin": "Bərkitmə", "column_header.unpin": "Bərkitmə",
"column_search.cancel": "İmtina", "column_search.cancel": "İmtina",
"community.column_settings.local_only": "Sadəcə lokalda", "community.column_settings.local_only": "Sadəcə lokalda",
@ -192,7 +207,7 @@
"compose_form.poll.type": "Stil", "compose_form.poll.type": "Stil",
"compose_form.publish": "Paylaş", "compose_form.publish": "Paylaş",
"compose_form.reply": "Cavabla", "compose_form.reply": "Cavabla",
"compose_form.save_changes": "Yenilə", "compose_form.save_changes": "Güncəllə",
"compose_form.spoiler.marked": "Məzmun xəbərdarlığını sil", "compose_form.spoiler.marked": "Məzmun xəbərdarlığını sil",
"compose_form.spoiler.unmarked": "Məzmun xəbərdarlığı əlavə et", "compose_form.spoiler.unmarked": "Məzmun xəbərdarlığı əlavə et",
"compose_form.spoiler_placeholder": "Məzmun xəbərdarlığı (məcburi deyil)", "compose_form.spoiler_placeholder": "Məzmun xəbərdarlığı (məcburi deyil)",
@ -204,6 +219,13 @@
"confirmations.delete_list.confirm": "Sil", "confirmations.delete_list.confirm": "Sil",
"confirmations.delete_list.message": "Bu siyahını həmişəlik silmək istədiyinizə əminsiniz?", "confirmations.delete_list.message": "Bu siyahını həmişəlik silmək istədiyinizə əminsiniz?",
"confirmations.delete_list.title": "Siyahı silinsin?", "confirmations.delete_list.title": "Siyahı silinsin?",
"confirmations.discard_draft.confirm": "Silib davam et",
"confirmations.discard_draft.edit.cancel": "Düzəliş etməyə davam",
"confirmations.discard_draft.edit.message": "Davam etsəniz, hazırda düzəliş etdiyiniz göndərişdəki bütün dəyişikliklər silinəcək.",
"confirmations.discard_draft.edit.title": "Göndərişinizə etdiyiniz bütün dəyişikliklər silinsin?",
"confirmations.discard_draft.post.cancel": "Qaralama kimi davam etdir",
"confirmations.discard_draft.post.message": "Davam etsəniz, hazırda tərtib etdiyiniz göndəriş silinəcək.",
"confirmations.discard_draft.post.title": "Qaralama göndərişiniz silinsin?",
"confirmations.discard_edit_media.confirm": "Ləğv et", "confirmations.discard_edit_media.confirm": "Ləğv et",
"confirmations.discard_edit_media.message": "Media təsvirində və ya önizləmədə yadda saxlanmamış dəyişiklikləriniz var, ləğv edilsin?", "confirmations.discard_edit_media.message": "Media təsvirində və ya önizləmədə yadda saxlanmamış dəyişiklikləriniz var, ləğv edilsin?",
"confirmations.follow_to_list.confirm": "İzlə və siyahıya əlavə et", "confirmations.follow_to_list.confirm": "İzlə və siyahıya əlavə et",
@ -213,13 +235,19 @@
"confirmations.logout.message": ıxmaq istədiyinizə əminsiniz?", "confirmations.logout.message": ıxmaq istədiyinizə əminsiniz?",
"confirmations.logout.title": ıxış edilsin?", "confirmations.logout.title": ıxış edilsin?",
"confirmations.missing_alt_text.confirm": "Alternativ mətn əlavə et", "confirmations.missing_alt_text.confirm": "Alternativ mətn əlavə et",
"confirmations.missing_alt_text.message": "Paylaşımınız alternativ mətn ehtiva etmir. Təsvir əlavə etmək onun daha çox insan üçün əlçatan olmasına kömək edir.", "confirmations.missing_alt_text.message": "Göndərişinizdə alternativ mətn yoxdur. Daha çox insanın məzmununuza erişməsinə kömək etmək üçün açıqlama əlavə edin.",
"confirmations.missing_alt_text.secondary": "Yenə də paylaş", "confirmations.missing_alt_text.secondary": "Yenə də paylaş",
"confirmations.missing_alt_text.title": "Alternativ mətn əlavə edilsin?", "confirmations.missing_alt_text.title": "Alternativ mətn əlavə edilsin?",
"confirmations.mute.confirm": "Səssizləşdir", "confirmations.mute.confirm": "Səssizləşdir",
"confirmations.redraft.confirm": "Sil və qaralamaya köçür", "confirmations.redraft.confirm": "Sil və qaralamaya köçür",
"confirmations.redraft.message": "Bu paylaşımı silmək və qaralamaya köçürmək istədiyinizə əminsiniz? Bəyənmələr və gücləndirmələr itəcək və orijinal paylaşıma olan cavablar tənha qalacaq.", "confirmations.redraft.message": "Bu paylaşımı silmək və qaralamaya köçürmək istədiyinizə əminsiniz? Bəyənmələr və gücləndirmələr itəcək və orijinal paylaşıma olan cavablar tənha qalacaq.",
"confirmations.redraft.title": "Paylaşım silinsin & qaralamaya köçürülsün?", "confirmations.redraft.title": "Paylaşım silinsin & qaralamaya köçürülsün?",
"confirmations.remove_from_followers.confirm": "İzləyicini çıxart",
"confirmations.remove_from_followers.message": "{name} sizi izləməyəcək. Davam etmək istədiyinizə əminsiniz?",
"confirmations.remove_from_followers.title": "İzləyici çıxarılsın?",
"confirmations.revoke_quote.confirm": "Göndərişi sil",
"confirmations.revoke_quote.message": "Bu əməliyyatın geri dönüşü yoxdur.",
"confirmations.revoke_quote.title": "Göndəriş silinsin?",
"confirmations.unfollow.confirm": "İzləmədən çıxar", "confirmations.unfollow.confirm": "İzləmədən çıxar",
"confirmations.unfollow.message": "{name} izləmədən çıxmaq istədiyinizə əminsiniz?", "confirmations.unfollow.message": "{name} izləmədən çıxmaq istədiyinizə əminsiniz?",
"confirmations.unfollow.title": "İstifadəçi izləmədən çıxarılsın?", "confirmations.unfollow.title": "İstifadəçi izləmədən çıxarılsın?",
@ -232,12 +260,12 @@
"conversation.with": "{names} ilə", "conversation.with": "{names} ilə",
"copy_icon_button.copied": "Mübadilə buferinə köçürüldü", "copy_icon_button.copied": "Mübadilə buferinə köçürüldü",
"copypaste.copied": "Kopyalandı", "copypaste.copied": "Kopyalandı",
"copypaste.copy_to_clipboard": "Kopyala", "copypaste.copy_to_clipboard": "Lövhəyə kopyala",
"directory.federated": "Bilinən fediversedən", "directory.federated": "Bilinən fediversedən",
"directory.local": "Sadəcə {domain}", "directory.local": "Sadəcə {domain}",
"directory.new_arrivals": "Yeni gələnlər", "directory.new_arrivals": "Yeni gələnlər",
"directory.recently_active": "Bayaq aktiv olanlar", "directory.recently_active": "Bayaq aktiv olanlar",
"disabled_account_banner.account_settings": "Hesab parametrləri", "disabled_account_banner.account_settings": "Hesab ayarları",
"disabled_account_banner.text": "Sizin hesabınız {disabledAccount} hal-hazırda deaktiv edilib.", "disabled_account_banner.text": "Sizin hesabınız {disabledAccount} hal-hazırda deaktiv edilib.",
"dismissable_banner.community_timeline": "Bunlar, hesabları {domain} serverində yerləşən insanların ən son ictimai paylaşımlarıdır.", "dismissable_banner.community_timeline": "Bunlar, hesabları {domain} serverində yerləşən insanların ən son ictimai paylaşımlarıdır.",
"dismissable_banner.dismiss": "Bağla", "dismissable_banner.dismiss": "Bağla",
@ -273,7 +301,7 @@
"emoji_button.food": "Yemək və içki", "emoji_button.food": "Yemək və içki",
"emoji_button.label": "Emoji daxil et", "emoji_button.label": "Emoji daxil et",
"emoji_button.nature": "Təbiət", "emoji_button.nature": "Təbiət",
"emoji_button.not_found": "Uyğun emoji tapılmadı", "emoji_button.not_found": "Uyuşan emoji tapılmadı",
"emoji_button.objects": "Obyektlər", "emoji_button.objects": "Obyektlər",
"emoji_button.people": "İnsanlar", "emoji_button.people": "İnsanlar",
"emoji_button.recent": "Tez-tez istifadə edilən", "emoji_button.recent": "Tez-tez istifadə edilən",
@ -296,6 +324,232 @@
"empty_column.follow_requests": "İzləmə sorğularınız yoxdur. Qəbul etdikdə burada görəcəksiniz.", "empty_column.follow_requests": "İzləmə sorğularınız yoxdur. Qəbul etdikdə burada görəcəksiniz.",
"empty_column.followed_tags": "Heç bir heşteq izləmirsiniz. İzlədikdə burada görünəcək.", "empty_column.followed_tags": "Heç bir heşteq izləmirsiniz. İzlədikdə burada görünəcək.",
"empty_column.hashtag": "Bu heşteqdə hələ ki, heç nə yoxdur.", "empty_column.hashtag": "Bu heşteqdə hələ ki, heç nə yoxdur.",
"empty_column.notification_requests": "Hamısı hazırdır! Burada heç nə yoxdur. Yeni bildiriş aldığınız zaman, ayarlarınıza görə burada görünəcək.",
"errors.unexpected_crash.report_issue": "Problemi bildir",
"explore.suggested_follows": "İnsanlar",
"explore.title": "Trendlər",
"explore.trending_links": "Xəbərlər",
"explore.trending_statuses": "Göndərişlər",
"explore.trending_tags": "Mövzu etiketləri",
"featured_carousel.header": "{count, plural, one {Sancılmış göndəriş} other {Sancılmış göndərişlər}}",
"featured_carousel.next": "Növbəti",
"featured_carousel.post": "Göndəriş",
"featured_carousel.previous": "Əvvəlki",
"featured_carousel.slide": "{index}/{total}",
"filter_modal.added.context_mismatch_explanation": "Bu filtr kateqoriyası, bu göndərişdə erişdiyiniz kontekstə aid deyil. Əgər göndərişin bu kontekstdə də filtrlənməsini istəyirsinizsə, filtrə düzəliş etməyiniz lazımdır.",
"filter_modal.added.context_mismatch_title": "Kontekst uyuşmur!",
"filter_modal.added.expired_explanation": "Bu filtr kateqoriyasının vaxtı bitib, filtri tətbiq etmək üçün bitmə tarixini dəyişdirməlisiniz.",
"filter_modal.added.expired_title": "Vaxtı bitmiş filtr!",
"filter_modal.added.review_and_configure": "Bu filt kateqoriyasını incələmək və daha detallı konfiqurasiya etmək üçün {settings_link} ünvanına gedin.",
"filter_modal.added.review_and_configure_title": "Filtr ayarları",
"filter_modal.added.settings_link": "ayarlar səhifəsi",
"filter_modal.added.short_explanation": "Bu göndəriş, aşağıdakı filtr kateqoriyasına əlavə edilib: {title}.",
"filter_modal.added.title": "Filtr əlavə edilib!",
"filter_warning.matches_filter": "“<span>{title}</span>” filtri ilə uyuşur",
"follow_suggestions.hints.friends_of_friends": "Bu profil izlədiyiniz insanlar arasında populyardır.", "follow_suggestions.hints.friends_of_friends": "Bu profil izlədiyiniz insanlar arasında populyardır.",
"follow_suggestions.hints.most_followed": "Bu profil {domain} serverində ən çox izlənilənlərdən biridir." "follow_suggestions.hints.most_followed": "Bu profil {domain} serverində ən çox izlənilənlərdən biridir.",
"generic.saved": "Saxlanıldı",
"getting_started.heading": "Başlayaq",
"hashtag.admin_moderation": "#{name} üçün moderasiya interfeysini aç",
"hashtag.browse": "#{hashtag} göndərişlərinə bax",
"hashtag.browse_from_account": "@{name} - #{hashtag} göndərişlərinə bax",
"hashtag.column_header.tag_mode.all": "və {additional}",
"hashtag.column_header.tag_mode.any": "və ya {additional}",
"hashtag.column_header.tag_mode.none": "{additional} olmadan",
"hashtag.column_settings.select.no_options_message": "Heç bir təklif tapılmadı",
"hashtag.column_settings.select.placeholder": "Mövzu etiketlərini daxil edin…",
"hashtag.column_settings.tag_mode.all": "Bunların hamısı",
"hashtag.column_settings.tag_mode.any": "Bunlardan hər hansısa biri",
"hashtag.column_settings.tag_mode.none": "Bunların heç biri",
"hashtag.column_settings.tag_toggle": "Bu sütun üçün əlavə etiketləri daxil et",
"hashtag.counter_by_accounts": "{count, plural, one {{counter} iştirakçı} other {{counter} iştirakçı}}",
"hashtag.counter_by_uses": "{count, plural, one {{counter} göndəriş} other {{counter} göndəriş}}",
"hashtag.counter_by_uses_today": "Bu gün {count, plural, one {{counter} göndəriş} other {{counter} göndəriş}}",
"hashtag.follow": "Mövzu etiketini izlə",
"hashtag.mute": "#{hashtag} - səssizə al",
"hashtag.unfollow": "Mövzu etiketini izləmə",
"hashtags.and_other": "…və daha {count, plural, one {}other {# ədəd}}",
"hints.profiles.followers_may_be_missing": "Bu profilin izləyiciləri əskik ola bilər.",
"hints.profiles.follows_may_be_missing": "Bu profilin izləyənləri əskik ola bilər.",
"home.column_settings.show_quotes": "Sitatları göstər",
"home.column_settings.show_replies": "Cavabları göstər",
"home.hide_announcements": "Elanları gizlət",
"home.pending_critical_update.body": "Lütfən Mastodon serverinizi mümkün olan ən qısa müddətdə güncəlləyin!",
"home.pending_critical_update.link": "Güncəlləmələrə bax",
"home.pending_critical_update.title": "Kritik güvənlik güncəlləməsi mövcuddur!",
"home.show_announcements": "Elanları göstər",
"ignore_notifications_modal.ignore": "Bildirişləri yox say",
"ignore_notifications_modal.limited_accounts_title": "Moderasiya edilmiş hesabların bildirişləri yox sayılsın?",
"ignore_notifications_modal.new_accounts_title": "Yeni hesabların bildirişləri yox sayılsın?",
"ignore_notifications_modal.not_followers_title": "Sizi izləməyən şəxslərin bildirişləri yox sayılsın?",
"ignore_notifications_modal.not_following_title": "İzləmədiyiniz şəxslərin bildirişləri yox sayılsın?",
"ignore_notifications_modal.private_mentions_title": "İstənilməyən Şəxsi Adçəkmələrdən gələn bildirişlər yox sayılsın?",
"info_button.label": "Kömək",
"interaction_modal.action.favourite": "Davam etmək üçün hesabınızdan sevimlilərə əlavə etməlisiniz.",
"interaction_modal.action.follow": "Davam etmək üçün hesabınızdan izləməlisiniz.",
"interaction_modal.action.reblog": "Davam etmək üçün hesabınızdan təkrar göndərməlisiniz.",
"interaction_modal.action.reply": "Davam etmək üçün hesabınızdan cavab verməlisiniz.",
"interaction_modal.action.vote": "Davam etmək üçün hesabınızdan səs verməlisiniz.",
"keyboard_shortcuts.profile": "Müəllifin profilini aç",
"keyboard_shortcuts.reply": "Göndərişə cavab ver",
"learn_more_link.got_it": "Anladım",
"learn_more_link.learn_more": "Daha ətraflı",
"lightbox.close": "Bağla",
"lightbox.next": "Növbəti",
"lightbox.previous": "Əvvəlki",
"lightbox.zoom_in": "Həqiqi ölçüyə qayıt",
"limited_account_hint.action": "Yenə də profili göstər",
"limited_account_hint.title": "Bu profil, {domain} moderatorları tərəfindən gizlədildi.",
"navigation_bar.account_settings": "Parol və təhlükəsizlik",
"navigation_bar.moderation": "Moderasiya",
"not_signed_in_indicator.not_signed_in": "Bu resursa erişmək üçün giriş etməlisiniz.",
"notification.moderation-warning.learn_more": "Daha ətraflı",
"notification.moderation_warning": "Bir moderasiya xəbərdarlığı aldınız",
"notification.moderation_warning.action_delete_statuses": "Bəzi göndərişləriniz silindi.",
"notification.moderation_warning.action_disable": "Hesabınız sıradan çıxarılıb.",
"notification.moderation_warning.action_mark_statuses_as_sensitive": "Bəzi göndərişləriniz həssas olaraq işarələnib.",
"notification.moderation_warning.action_none": "Hesabınız bir moderasiya xəbərdarlığı aldı.",
"notification.moderation_warning.action_sensitive": "Göndərişləriniz artıq həssas olaraq işarələnəcək.",
"notification.moderation_warning.action_silence": "Hesabınız məhdudlaşdırılıb.",
"notification.moderation_warning.action_suspend": "Hesabınızın fəaliyyəti dayandırılıb.",
"notification_requests.confirm_dismiss_multiple.message": "{count, plural, one {bir bildiriş sorğusunu} other {# bildiriş sorğusunu}} bağlamaq üzrəsiniz. {count, plural, one {Ona} other {Onlara}} yenidən asanlıqla erişə bilməyəcəksiniz. Davam etmək istədiyinizə əminsiniz?",
"notification_requests.explainer_for_limited_account": "Hesab, bir moderator tərəfindən məhdudlaşdırıldığı üçün bu hesabın bildirişləri filtrləndi.",
"notification_requests.explainer_for_limited_remote_account": "Hesab və ya onun serveri, bir moderator tərəfindən məhdudlaşdırıldığı üçün bu hesabın bildirişləri filtrləndi.",
"notifications.filter.statuses": "İzlədiyiniz şəxslərdən güncəlləmələr",
"notifications.policy.filter_limited_accounts_hint": "Server moderatorları tərəfindən məhdudlaşdırılıb",
"notifications.policy.filter_limited_accounts_title": "Moderasiya edilmiş hesablar",
"password_confirmation.exceeds_maxlength": "Parol təsdiqi, maksimum parol uzunluğunu aşır",
"password_confirmation.mismatching": "Parol təsdiqi uyuşmur",
"privacy_policy.last_updated": "Son güncəlləmə {date}",
"report.category.subtitle": "Ən çox uyuşanı seçin",
"report_notification.categories.spam_sentence": "spam",
"report_notification.categories.violation": "Qayda pozuntusu",
"report_notification.categories.violation_sentence": "qayda pozuntusu",
"report_notification.open": "Hesabatı aç",
"search.clear": "Axtarışı təmizlə",
"search.no_recent_searches": "Son axtarışlar yoxdur",
"search.placeholder": "Axtar",
"search.quick_action.account_search": "Uyuşan profillər {x}",
"search.quick_action.go_to_account": "{x} profilinə get",
"search.quick_action.go_to_hashtag": "{x} mövzu etiketinə get",
"search.quick_action.open_url": "URL-ni Mastodon-da aç",
"search.quick_action.status_search": "Uyuşan göndərişlər {x}",
"search.search_or_paste": "Axtar və ya URL-ni yapışdır",
"search_popout.full_text_search_disabled_message": "{domain} domenində mövcud deyil.",
"search_popout.full_text_search_logged_out_message": "Yalnız giriş edildiyi zaman əlçatandır.",
"search_popout.language_code": "ISO dil kodu",
"search_popout.options": "Axtarış seçimləri",
"search_popout.quick_actions": "Cəld əməliyyatlar",
"search_popout.recent": "Son axtarışlar",
"search_popout.specific_date": "müəyyən tarix",
"search_popout.user": "istifadəçi",
"search_results.accounts": "Profillər",
"search_results.all": "Hamısı",
"search_results.hashtags": "Mövzu etiketləri",
"search_results.no_results": "Nəticə yoxdur.",
"search_results.no_search_yet": "Göndərişləri, profilləri və ya mövzu etiketlərini axtarmağa çalışın.",
"search_results.see_all": "Hamısına bax",
"search_results.statuses": "Göndərişlər",
"search_results.title": "\"{q}\" axtar",
"server_banner.about_active_users": "Son 30 gündə bu serveri istifadə edənlər (aylıq aktiv istifadəçilər)",
"server_banner.active_users": "aktiv istifadəçilər",
"server_banner.is_one_of_many": "{domain}, fediverse-də iştirak etmək üçün istifadə edə biləcəyiniz bir neçə müstəqil Mastodon serverlərindən biridir.",
"server_banner.server_stats": "Server statistikaları:",
"sign_in_banner.create_account": "Hesab yarat",
"sign_in_banner.follow_anyone": "fediverse-dəki hər kəsi izləyin və hamısına xronoloji ardıcıllıqla baxın. Heç bir alqoritm, reklam və ya klikləmə tələsi yoxdur.",
"sign_in_banner.mastodon_is": "Mastodon, baş verənlərdən xəbərdar olmağın ən yaxşı yoldur.",
"sign_in_banner.sign_in": "Giriş",
"sign_in_banner.sso_redirect": "Giriş və ya Qeydiyyat",
"status.admin_account": "@{name} üçün moderasiya interfeysini aç",
"status.admin_domain": "{domain} üçün moderasiya interfeysini aç",
"status.admin_status": "Moderasiya interfeysində bu göndərişi aç",
"status.block": "Əngəllə: @{name}",
"status.bookmark": "Əlfəcin",
"status.context.load_new_replies": "Yeni cavablar mövcuddur",
"status.context.loading": "Daha çox cavab yoxlanılır",
"status.delete": "Sil",
"status.direct": "Şəxsi olaraq adını çək: @{name}",
"status.direct_indicator": "Şəxsi olaraq adını çək",
"status.edit": "Düzəliş et",
"status.edited": "Son düzəliş {date}",
"status.edited_x_times": "{count, plural, one {{count} dəfə} other {{count} dəfə}} düzəliş edilib",
"status.favourite": "Sevimli",
"status.favourites": "{count, plural, one {sevimli} other {sevimli}}",
"status.filter": "Bu göndərişi filtrlə",
"status.history.created": "{name}, {date} yaratdı",
"status.history.edited": "{name}, {date} düzəliş etdi",
"status.load_more": "Daha çoxunu yüklə",
"status.media.open": "Açmaq üçün kliklə",
"status.media.show": "Göstərmək üçün kliklə",
"status.media_hidden": "Media gizlidir",
"status.mention": "Adını çək: @{name}",
"status.more": "Daha çox",
"status.mute": "@{name} - səssizə al",
"status.mute_conversation": "Danışığın səsini kəs",
"status.open": "Bu göndərişi genişləndir",
"status.quote_error.filtered": "Bəzi filtrlərinizə görə gizlidir",
"status.quote_error.not_available": "Göndəriş əlçatmazdır",
"status.quote_error.pending_approval": "Göndəriş gözləmədədir",
"status.read_more": "Daha çoxunu oxu",
"status.remove_bookmark": "Əlfəcini sil",
"status.remove_favourite": "Sevimlilərdən sil",
"status.replied_to": "Cavab verildi: {name}",
"status.reply": "Cavabla",
"status.report": "Bildir: @{name}",
"status.sensitive_warning": "Həssas məzmun",
"status.share": "Paylaş",
"status.show_less_all": "Hamısı üçün daha az göstər",
"status.show_more_all": "Hamısı üçün daha çox göstər",
"status.show_original": "Orijinalı göstər",
"status.translate": "Tərcümə et",
"status.translated_from_with": "{provider} ilə {lang} dilindən tərcümə edilib",
"status.uncached_media_warning": "Önizləmə mövcud deyil",
"status.unmute_conversation": "Danışığın səsini aç",
"subscribed_languages.save": "Dəyişiklikləri saxla",
"subscribed_languages.target": "{target} üçün abunə olunmuş dilləri dəyişdir",
"tabs_bar.home": "Ana səhifə",
"tabs_bar.menu": "Menyu",
"tabs_bar.notifications": "Bildirişlər",
"tabs_bar.publish": "Yeni göndəriş",
"tabs_bar.search": "Axtar",
"terms_of_service.effective_as_of": "{date} etibarilə qüvvədə",
"terms_of_service.title": "Xidmət Şərtləri",
"terms_of_service.upcoming_changes_on": "{date} tarixində ediləcək dəyişikliklər",
"time_remaining.days": "{number, plural, one {# gün} other {# gün}} qalıb",
"time_remaining.hours": "{number, plural, one {# saat} other {# saat}} qalıb",
"time_remaining.minutes": "{number, plural, one {# dəqiqə} other {# dəqiqə}} qalıb",
"time_remaining.moments": "Bir neçə dəqiqə qalıb",
"time_remaining.seconds": "{number, plural, one {# saniyə} other {# saniyə}} qalıb",
"trends.trending_now": "İndi trenddədir",
"ui.beforeunload": "Mastodon-u tərk etsəniz, qaralamanız itəcək.",
"units.short.billion": "{count} mlyrd",
"units.short.million": "{count} mlyn",
"units.short.thousand": "{count} min",
"upload_area.title": "Yükləmək üçün sürüklə və burax",
"upload_button.label": "Təsvir, video və ya səs faylı əlavə et",
"upload_error.limit": "Fayl yükləmə limiti aşılıb.",
"upload_error.poll": "Anketlərdə fayl yükləməyə icazə verilmir.",
"upload_form.drag_and_drop.instructions": "Bir media qoşmasını daşımaq üçün boşluq və ya enter düyməsinə basın. Sürükləmə zamanı, media qoşmasını hər hansısa bir yönə hərəkət etdirmək üçün ox düymələrini istifadə edin. Media qoşmasını yeni mövqeyinə buraxmaq üçün təkrar boşluq və ya enter düyməsinə basın, ləğv etmək üçün escape düyməsinə basın.",
"upload_form.drag_and_drop.on_drag_cancel": "Sürükləmə ləğv edilib. {item} media qoşması buraxıldı.",
"upload_form.drag_and_drop.on_drag_end": "{item} media qoşması buraxıldı.",
"upload_form.drag_and_drop.on_drag_over": "{item} media qoşması daşındı.",
"upload_form.drag_and_drop.on_drag_start": "{item} media qoşması alındı.",
"upload_form.edit": "Düzəliş et",
"upload_progress.label": "Yüklənir...",
"upload_progress.processing": "Emal edilir…",
"username.taken": "Bu istifadəçi adı götürülüb. Başqasını sınayın",
"video.close": "Videonu bağla",
"video.download": "Faylı endir",
"video.exit_fullscreen": "Tam ekrandan çıx",
"video.expand": "Videonu genişləndir",
"video.fullscreen": "Tam ekran",
"video.hide": "Videonu gizlət",
"video.mute": "Səsi kəs",
"video.pause": "Fasilə ver",
"video.play": "Oxut",
"video.skip_backward": "Geri ötür",
"video.skip_forward": "İrəli ötür",
"video.unmute": "Səsi aç",
"video.volume_down": "Həcmi azalt",
"video.volume_up": "Həcmi artır"
} }

View File

@ -404,8 +404,6 @@
"hints.profiles.see_more_followers": "Глядзець больш падпісаных на {domain}", "hints.profiles.see_more_followers": "Глядзець больш падпісаных на {domain}",
"hints.profiles.see_more_follows": "Глядзець больш падпісак на {domain}", "hints.profiles.see_more_follows": "Глядзець больш падпісак на {domain}",
"hints.profiles.see_more_posts": "Глядзець больш допісаў на {domain}", "hints.profiles.see_more_posts": "Глядзець больш допісаў на {domain}",
"hints.threads.replies_may_be_missing": "Адказы з іншых сервераў могуць адсутнічаць.",
"hints.threads.see_more": "Глядзіце больш адказаў на {domain}",
"home.column_settings.show_quotes": "Паказаць цытаты", "home.column_settings.show_quotes": "Паказаць цытаты",
"home.column_settings.show_reblogs": "Паказваць пашырэнні", "home.column_settings.show_reblogs": "Паказваць пашырэнні",
"home.column_settings.show_replies": "Паказваць адказы", "home.column_settings.show_replies": "Паказваць адказы",
@ -823,7 +821,6 @@
"status.mute_conversation": "Ігнараваць размову", "status.mute_conversation": "Ігнараваць размову",
"status.open": "Разгарнуць гэты допіс", "status.open": "Разгарнуць гэты допіс",
"status.pin": "Замацаваць у профілі", "status.pin": "Замацаваць у профілі",
"status.quote_post_author": "Допіс карыстальніка @{name}",
"status.read_more": "Чытаць болей", "status.read_more": "Чытаць болей",
"status.reblog": "Пашырыць", "status.reblog": "Пашырыць",
"status.reblog_private": "Пашырыць з першапачатковай бачнасцю", "status.reblog_private": "Пашырыць з першапачатковай бачнасцю",

View File

@ -419,8 +419,6 @@
"hints.profiles.see_more_followers": "Преглед на още последователи на {domain}", "hints.profiles.see_more_followers": "Преглед на още последователи на {domain}",
"hints.profiles.see_more_follows": "Преглед на още последвания на {domain}", "hints.profiles.see_more_follows": "Преглед на още последвания на {domain}",
"hints.profiles.see_more_posts": "Преглед на още публикации на {domain}", "hints.profiles.see_more_posts": "Преглед на още публикации на {domain}",
"hints.threads.replies_may_be_missing": "Отговори от други сървъри може да липсват.",
"hints.threads.see_more": "Преглед на още отговори на {domain}",
"home.column_settings.show_quotes": "Показване на цитираното", "home.column_settings.show_quotes": "Показване на цитираното",
"home.column_settings.show_reblogs": "Показване на подсилванията", "home.column_settings.show_reblogs": "Показване на подсилванията",
"home.column_settings.show_replies": "Показване на отговорите", "home.column_settings.show_replies": "Показване на отговорите",
@ -862,12 +860,6 @@
"status.open": "Разширяване на публикацията", "status.open": "Разширяване на публикацията",
"status.pin": "Закачане в профила", "status.pin": "Закачане в профила",
"status.quote_error.filtered": "Скрито поради един от филтрите ви", "status.quote_error.filtered": "Скрито поради един от филтрите ви",
"status.quote_error.not_found": "Публикацията не може да се показва.",
"status.quote_error.pending_approval": "Публикацията чака одобрение от първоначалния автор.",
"status.quote_error.rejected": "Публикацията не може да се показва като първоначалния автор не позволява цитирането ѝ.",
"status.quote_error.removed": "Публикацията е премахната от автора ѝ.",
"status.quote_error.unauthorized": "Публикацията не може да се показва, тъй като не сте упълномощени да я гледате.",
"status.quote_post_author": "Публикация от {name}",
"status.read_more": "Още за четене", "status.read_more": "Още за четене",
"status.reblog": "Подсилване", "status.reblog": "Подсилване",
"status.reblog_private": "Подсилване с оригиналната видимост", "status.reblog_private": "Подсилване с оригиналната видимост",

View File

@ -558,6 +558,8 @@
"status.bookmark": "Ouzhpennañ d'ar sinedoù", "status.bookmark": "Ouzhpennañ d'ar sinedoù",
"status.cancel_reblog_private": "Nac'hañ ar skignadenn", "status.cancel_reblog_private": "Nac'hañ ar skignadenn",
"status.cannot_reblog": "Ar c'hannad-se na c'hall ket bezañ skignet", "status.cannot_reblog": "Ar c'hannad-se na c'hall ket bezañ skignet",
"status.context.load_new_replies": "Respontoù nevez zo",
"status.context.loading": "O kerc'hat muioc'h a respontoù",
"status.copy": "Eilañ liamm ar c'hannad", "status.copy": "Eilañ liamm ar c'hannad",
"status.delete": "Dilemel", "status.delete": "Dilemel",
"status.detailed_status": "Gwel kaozeadenn munudek", "status.detailed_status": "Gwel kaozeadenn munudek",
@ -580,7 +582,6 @@
"status.mute_conversation": "Kuzhat ar gaozeadenn", "status.mute_conversation": "Kuzhat ar gaozeadenn",
"status.open": "Digeriñ ar c'hannad-mañ", "status.open": "Digeriñ ar c'hannad-mañ",
"status.pin": "Spilhennañ d'ar profil", "status.pin": "Spilhennañ d'ar profil",
"status.quote_post_author": "Embannadenn gant {name}",
"status.read_more": "Lenn muioc'h", "status.read_more": "Lenn muioc'h",
"status.reblog": "Skignañ", "status.reblog": "Skignañ",
"status.reblog_private": "Skignañ gant ar weledenn gentañ", "status.reblog_private": "Skignañ gant ar weledenn gentañ",

View File

@ -245,6 +245,7 @@
"confirmations.remove_from_followers.confirm": "Elimina el seguidor", "confirmations.remove_from_followers.confirm": "Elimina el seguidor",
"confirmations.remove_from_followers.message": "{name} deixarà de seguir-vos. Tirem endavant?", "confirmations.remove_from_followers.message": "{name} deixarà de seguir-vos. Tirem endavant?",
"confirmations.remove_from_followers.title": "Eliminem el seguidor?", "confirmations.remove_from_followers.title": "Eliminem el seguidor?",
"confirmations.revoke_quote.message": "Aquesta acció no es pot desfer.",
"confirmations.unfollow.confirm": "Deixa de seguir", "confirmations.unfollow.confirm": "Deixa de seguir",
"confirmations.unfollow.message": "Segur que vols deixar de seguir {name}?", "confirmations.unfollow.message": "Segur que vols deixar de seguir {name}?",
"confirmations.unfollow.title": "Deixar de seguir l'usuari?", "confirmations.unfollow.title": "Deixar de seguir l'usuari?",
@ -424,8 +425,6 @@
"hints.profiles.see_more_followers": "Vegeu més seguidors a {domain}", "hints.profiles.see_more_followers": "Vegeu més seguidors a {domain}",
"hints.profiles.see_more_follows": "Vegeu més seguiments a {domain}", "hints.profiles.see_more_follows": "Vegeu més seguiments a {domain}",
"hints.profiles.see_more_posts": "Vegeu més publicacions a {domain}", "hints.profiles.see_more_posts": "Vegeu més publicacions a {domain}",
"hints.threads.replies_may_be_missing": "Es poden haver perdut respostes d'altres servidors.",
"hints.threads.see_more": "Vegeu més respostes a {domain}",
"home.column_settings.show_quotes": "Mostrar les cites", "home.column_settings.show_quotes": "Mostrar les cites",
"home.column_settings.show_reblogs": "Mostra els impulsos", "home.column_settings.show_reblogs": "Mostra els impulsos",
"home.column_settings.show_replies": "Mostra les respostes", "home.column_settings.show_replies": "Mostra les respostes",
@ -499,6 +498,8 @@
"keyboard_shortcuts.translate": "per a traduir una publicació", "keyboard_shortcuts.translate": "per a traduir una publicació",
"keyboard_shortcuts.unfocus": "Descentra l'àrea de composició de text/cerca", "keyboard_shortcuts.unfocus": "Descentra l'àrea de composició de text/cerca",
"keyboard_shortcuts.up": "Apuja a la llista", "keyboard_shortcuts.up": "Apuja a la llista",
"learn_more_link.got_it": "Entesos",
"learn_more_link.learn_more": "Per a saber-ne més",
"lightbox.close": "Tanca", "lightbox.close": "Tanca",
"lightbox.next": "Següent", "lightbox.next": "Següent",
"lightbox.previous": "Anterior", "lightbox.previous": "Anterior",
@ -599,6 +600,7 @@
"notification.label.mention": "Menció", "notification.label.mention": "Menció",
"notification.label.private_mention": "Menció privada", "notification.label.private_mention": "Menció privada",
"notification.label.private_reply": "Resposta en privat", "notification.label.private_reply": "Resposta en privat",
"notification.label.quote": "{name} ha citat la vostra publicació",
"notification.label.reply": "Resposta", "notification.label.reply": "Resposta",
"notification.mention": "Menció", "notification.mention": "Menció",
"notification.mentioned_you": "{name} us ha mencionat", "notification.mentioned_you": "{name} us ha mencionat",
@ -656,6 +658,7 @@
"notifications.column_settings.mention": "Mencions:", "notifications.column_settings.mention": "Mencions:",
"notifications.column_settings.poll": "Resultats de lenquesta:", "notifications.column_settings.poll": "Resultats de lenquesta:",
"notifications.column_settings.push": "Notificacions push", "notifications.column_settings.push": "Notificacions push",
"notifications.column_settings.quote": "Cites:",
"notifications.column_settings.reblog": "Impulsos:", "notifications.column_settings.reblog": "Impulsos:",
"notifications.column_settings.show": "Mostra a la columna", "notifications.column_settings.show": "Mostra a la columna",
"notifications.column_settings.sound": "Reprodueix so", "notifications.column_settings.sound": "Reprodueix so",
@ -846,6 +849,8 @@
"status.bookmark": "Marca", "status.bookmark": "Marca",
"status.cancel_reblog_private": "Desfés l'impuls", "status.cancel_reblog_private": "Desfés l'impuls",
"status.cannot_reblog": "No es pot impulsar aquest tut", "status.cannot_reblog": "No es pot impulsar aquest tut",
"status.context.load_new_replies": "Hi ha respostes noves",
"status.context.loading": "Comprovació de més respostes",
"status.continued_thread": "Continuació del fil", "status.continued_thread": "Continuació del fil",
"status.copy": "Copia l'enllaç al tut", "status.copy": "Copia l'enllaç al tut",
"status.delete": "Elimina", "status.delete": "Elimina",
@ -872,12 +877,11 @@
"status.open": "Amplia el tut", "status.open": "Amplia el tut",
"status.pin": "Fixa en el perfil", "status.pin": "Fixa en el perfil",
"status.quote_error.filtered": "No es mostra a causa d'un dels vostres filtres", "status.quote_error.filtered": "No es mostra a causa d'un dels vostres filtres",
"status.quote_error.not_found": "No es pot mostrar aquesta publicació.", "status.quote_error.not_available": "Publicació no disponible",
"status.quote_error.pending_approval": "Aquesta publicació està pendent d'aprovació per l'autor original.", "status.quote_error.pending_approval": "Publicació pendent",
"status.quote_error.rejected": "No es pot mostrar aquesta publicació perquè l'autor original no en permet la citació.", "status.quote_error.pending_approval_popout.body": "Les citacions compartides a través del Fediverse poden trigar en aparèixer, perquè diferents servidors tenen diferents protocols.",
"status.quote_error.removed": "Aquesta publicació ha estat eliminada per l'autor.", "status.quote_error.pending_approval_popout.title": "Publicació pendent? Mantinguem la calma",
"status.quote_error.unauthorized": "No es pot mostrar aquesta publicació perquè no teniu autorització per a veure-la.", "status.quote_post_author": "S'ha citat una publicació de @{name}",
"status.quote_post_author": "Publicació de {name}",
"status.read_more": "Més informació", "status.read_more": "Més informació",
"status.reblog": "Impulsa", "status.reblog": "Impulsa",
"status.reblog_private": "Impulsa amb la visibilitat original", "status.reblog_private": "Impulsa amb la visibilitat original",
@ -892,6 +896,7 @@
"status.reply": "Respon", "status.reply": "Respon",
"status.replyAll": "Respon al fil", "status.replyAll": "Respon al fil",
"status.report": "Denuncia @{name}", "status.report": "Denuncia @{name}",
"status.revoke_quote": "Elimina la meva publicació de la de @{name}",
"status.sensitive_warning": "Contingut sensible", "status.sensitive_warning": "Contingut sensible",
"status.share": "Comparteix", "status.share": "Comparteix",
"status.show_less_all": "Mostra'n menys per a tot", "status.show_less_all": "Mostra'n menys per a tot",

View File

@ -245,6 +245,9 @@
"confirmations.remove_from_followers.confirm": "Odstranit sledujícího", "confirmations.remove_from_followers.confirm": "Odstranit sledujícího",
"confirmations.remove_from_followers.message": "{name} vás přestane sledovat. Jste si jisti, že chcete pokračovat?", "confirmations.remove_from_followers.message": "{name} vás přestane sledovat. Jste si jisti, že chcete pokračovat?",
"confirmations.remove_from_followers.title": "Odstranit sledujícího?", "confirmations.remove_from_followers.title": "Odstranit sledujícího?",
"confirmations.revoke_quote.confirm": "Odstranit příspěvek",
"confirmations.revoke_quote.message": "Tuto akci nelze vrátit zpět.",
"confirmations.revoke_quote.title": "Odstranit příspěvek?",
"confirmations.unfollow.confirm": "Přestat sledovat", "confirmations.unfollow.confirm": "Přestat sledovat",
"confirmations.unfollow.message": "Opravdu chcete {name} přestat sledovat?", "confirmations.unfollow.message": "Opravdu chcete {name} přestat sledovat?",
"confirmations.unfollow.title": "Přestat sledovat uživatele?", "confirmations.unfollow.title": "Přestat sledovat uživatele?",
@ -424,8 +427,6 @@
"hints.profiles.see_more_followers": "Zobrazit více sledujících na {domain}", "hints.profiles.see_more_followers": "Zobrazit více sledujících na {domain}",
"hints.profiles.see_more_follows": "Zobrazit další sledování na {domain}", "hints.profiles.see_more_follows": "Zobrazit další sledování na {domain}",
"hints.profiles.see_more_posts": "Zobrazit další příspěvky na {domain}", "hints.profiles.see_more_posts": "Zobrazit další příspěvky na {domain}",
"hints.threads.replies_may_be_missing": "Odpovědi z jiných serverů mohou chybět.",
"hints.threads.see_more": "Zobrazit další odpovědi na {domain}",
"home.column_settings.show_quotes": "Zobrazit citace", "home.column_settings.show_quotes": "Zobrazit citace",
"home.column_settings.show_reblogs": "Zobrazit boosty", "home.column_settings.show_reblogs": "Zobrazit boosty",
"home.column_settings.show_replies": "Zobrazit odpovědi", "home.column_settings.show_replies": "Zobrazit odpovědi",
@ -500,6 +501,8 @@
"keyboard_shortcuts.translate": "k přeložení příspěvku", "keyboard_shortcuts.translate": "k přeložení příspěvku",
"keyboard_shortcuts.unfocus": "Zrušit zaměření na nový příspěvek/hledání", "keyboard_shortcuts.unfocus": "Zrušit zaměření na nový příspěvek/hledání",
"keyboard_shortcuts.up": "Posunout v seznamu nahoru", "keyboard_shortcuts.up": "Posunout v seznamu nahoru",
"learn_more_link.got_it": "Rozumím",
"learn_more_link.learn_more": "Zjistit více",
"lightbox.close": "Zavřít", "lightbox.close": "Zavřít",
"lightbox.next": "Další", "lightbox.next": "Další",
"lightbox.previous": "Předchozí", "lightbox.previous": "Předchozí",
@ -600,6 +603,7 @@
"notification.label.mention": "Zmínka", "notification.label.mention": "Zmínka",
"notification.label.private_mention": "Soukromá zmínka", "notification.label.private_mention": "Soukromá zmínka",
"notification.label.private_reply": "Privátní odpověď", "notification.label.private_reply": "Privátní odpověď",
"notification.label.quote": "{name} citovali váš příspěvek",
"notification.label.reply": "Odpověď", "notification.label.reply": "Odpověď",
"notification.mention": "Zmínka", "notification.mention": "Zmínka",
"notification.mentioned_you": "{name} vás zmínil", "notification.mentioned_you": "{name} vás zmínil",
@ -657,6 +661,7 @@
"notifications.column_settings.mention": "Zmínky:", "notifications.column_settings.mention": "Zmínky:",
"notifications.column_settings.poll": "Výsledky anket:", "notifications.column_settings.poll": "Výsledky anket:",
"notifications.column_settings.push": "Push oznámení", "notifications.column_settings.push": "Push oznámení",
"notifications.column_settings.quote": "Citace:",
"notifications.column_settings.reblog": "Boosty:", "notifications.column_settings.reblog": "Boosty:",
"notifications.column_settings.show": "Zobrazit ve sloupci", "notifications.column_settings.show": "Zobrazit ve sloupci",
"notifications.column_settings.sound": "Přehrát zvuk", "notifications.column_settings.sound": "Přehrát zvuk",
@ -847,6 +852,8 @@
"status.bookmark": "Přidat do záložek", "status.bookmark": "Přidat do záložek",
"status.cancel_reblog_private": "Zrušit boostnutí", "status.cancel_reblog_private": "Zrušit boostnutí",
"status.cannot_reblog": "Tento příspěvek nemůže být boostnutý", "status.cannot_reblog": "Tento příspěvek nemůže být boostnutý",
"status.context.load_new_replies": "K dispozici jsou nové odpovědi",
"status.context.loading": "Hledání dalších odpovědí",
"status.continued_thread": "Pokračuje ve vlákně", "status.continued_thread": "Pokračuje ve vlákně",
"status.copy": "Zkopírovat odkaz na příspěvek", "status.copy": "Zkopírovat odkaz na příspěvek",
"status.delete": "Smazat", "status.delete": "Smazat",
@ -873,12 +880,11 @@
"status.open": "Rozbalit tento příspěvek", "status.open": "Rozbalit tento příspěvek",
"status.pin": "Připnout na profil", "status.pin": "Připnout na profil",
"status.quote_error.filtered": "Skryté kvůli jednomu z vašich filtrů", "status.quote_error.filtered": "Skryté kvůli jednomu z vašich filtrů",
"status.quote_error.not_found": "Tento příspěvek nelze zobrazit.", "status.quote_error.not_available": "Příspěvek není dostupný",
"status.quote_error.pending_approval": "Tento příspěvek čeká na schválení od původního autora.", "status.quote_error.pending_approval": "Příspěvek čeká na schválení",
"status.quote_error.rejected": "Tento příspěvek nemůže být zobrazen, protože původní autor neumožňuje, aby byl citován.", "status.quote_error.pending_approval_popout.body": "Zobrazení citátů sdílených napříč Fediversem může chvíli trvat, protože různé servery používají různé protokoly.",
"status.quote_error.removed": "Tento příspěvek byl odstraněn jeho autorem.", "status.quote_error.pending_approval_popout.title": "Příspěvek čeká na schválení? Buďte klidní",
"status.quote_error.unauthorized": "Tento příspěvek nelze zobrazit, protože nemáte oprávnění k jeho zobrazení.", "status.quote_post_author": "Citovali příspěvek od @{name}",
"status.quote_post_author": "Příspěvek od {name}",
"status.read_more": "Číst více", "status.read_more": "Číst více",
"status.reblog": "Boostnout", "status.reblog": "Boostnout",
"status.reblog_private": "Boostnout s původní viditelností", "status.reblog_private": "Boostnout s původní viditelností",
@ -893,6 +899,7 @@
"status.reply": "Odpovědět", "status.reply": "Odpovědět",
"status.replyAll": "Odpovědět na vlákno", "status.replyAll": "Odpovědět na vlákno",
"status.report": "Nahlásit @{name}", "status.report": "Nahlásit @{name}",
"status.revoke_quote": "Odstranit můj příspěvek z příspěvku @{name}",
"status.sensitive_warning": "Citlivý obsah", "status.sensitive_warning": "Citlivý obsah",
"status.share": "Sdílet", "status.share": "Sdílet",
"status.show_less_all": "Zobrazit méně pro všechny", "status.show_less_all": "Zobrazit méně pro všechny",

View File

@ -424,8 +424,6 @@
"hints.profiles.see_more_followers": "Gweld mwy o ddilynwyr ar {domain}", "hints.profiles.see_more_followers": "Gweld mwy o ddilynwyr ar {domain}",
"hints.profiles.see_more_follows": "Gweld mwy o 'yn dilyn' ar {domain}", "hints.profiles.see_more_follows": "Gweld mwy o 'yn dilyn' ar {domain}",
"hints.profiles.see_more_posts": "Gweld mwy o bostiadau ar {domain}", "hints.profiles.see_more_posts": "Gweld mwy o bostiadau ar {domain}",
"hints.threads.replies_may_be_missing": "Mae'n bosibl y bydd ymatebion gan weinyddion eraill ar goll.",
"hints.threads.see_more": "Gweld mwy o ymatebion ar {domain}",
"home.column_settings.show_quotes": "Dangos dyfyniadau", "home.column_settings.show_quotes": "Dangos dyfyniadau",
"home.column_settings.show_reblogs": "Dangos hybiau", "home.column_settings.show_reblogs": "Dangos hybiau",
"home.column_settings.show_replies": "Dangos ymatebion", "home.column_settings.show_replies": "Dangos ymatebion",
@ -873,12 +871,6 @@
"status.open": "Ehangu'r post hwn", "status.open": "Ehangu'r post hwn",
"status.pin": "Pinio ar y proffil", "status.pin": "Pinio ar y proffil",
"status.quote_error.filtered": "Wedi'i guddio oherwydd un o'ch hidlwyr", "status.quote_error.filtered": "Wedi'i guddio oherwydd un o'ch hidlwyr",
"status.quote_error.not_found": "Does dim modd dangos y postiad hwn.",
"status.quote_error.pending_approval": "Mae'r postiad hwn yn aros am gymeradwyaeth yr awdur gwreiddiol.",
"status.quote_error.rejected": "Does dim modd dangos y postiad hwn gan nad yw'r awdur gwreiddiol yn caniatáu iddo gael ei ddyfynnu.",
"status.quote_error.removed": "Cafodd y postiad hwn ei ddileu gan ei awdur.",
"status.quote_error.unauthorized": "Does dim modd dangos y postiad hwn gan nad oes gennych awdurdod i'w weld.",
"status.quote_post_author": "Postiad gan {name}",
"status.read_more": "Darllen rhagor", "status.read_more": "Darllen rhagor",
"status.reblog": "Hybu", "status.reblog": "Hybu",
"status.reblog_private": "Hybu i'r gynulleidfa wreiddiol", "status.reblog_private": "Hybu i'r gynulleidfa wreiddiol",

View File

@ -245,6 +245,9 @@
"confirmations.remove_from_followers.confirm": "Fjern følger", "confirmations.remove_from_followers.confirm": "Fjern følger",
"confirmations.remove_from_followers.message": "{name} vil ikke længere følge dig. Er du sikker på, at du vil fortsætte?", "confirmations.remove_from_followers.message": "{name} vil ikke længere følge dig. Er du sikker på, at du vil fortsætte?",
"confirmations.remove_from_followers.title": "Fjern følger?", "confirmations.remove_from_followers.title": "Fjern følger?",
"confirmations.revoke_quote.confirm": "Fjern indlæg",
"confirmations.revoke_quote.message": "Denne handling kan ikke fortrydes.",
"confirmations.revoke_quote.title": "Fjern indlæg?",
"confirmations.unfollow.confirm": "Følg ikke længere", "confirmations.unfollow.confirm": "Følg ikke længere",
"confirmations.unfollow.message": "Er du sikker på, at du ikke længere vil følge {name}?", "confirmations.unfollow.message": "Er du sikker på, at du ikke længere vil følge {name}?",
"confirmations.unfollow.title": "Følg ikke længere bruger?", "confirmations.unfollow.title": "Følg ikke længere bruger?",
@ -324,7 +327,7 @@
"empty_column.follow_requests": "Du har endnu ingen følgeanmodninger. Når du modtager én, vil den dukke op her.", "empty_column.follow_requests": "Du har endnu ingen følgeanmodninger. Når du modtager én, vil den dukke op her.",
"empty_column.followed_tags": "Ingen hashtags følges endnu. Når det sker, vil de fremgå her.", "empty_column.followed_tags": "Ingen hashtags følges endnu. Når det sker, vil de fremgå her.",
"empty_column.hashtag": "Der er intet med dette hashtag endnu.", "empty_column.hashtag": "Der er intet med dette hashtag endnu.",
"empty_column.home": "Din hjemmetidslinje er tom! Følg nogle personer, for at fylde den op.", "empty_column.home": "Din hjem-tidslinje er tom! Følg nogle personer, for at fylde den op.",
"empty_column.list": "Der er ikke noget på denne liste endnu. Når medlemmer af denne liste udgiver nye indlæg, vil de blive vist her.", "empty_column.list": "Der er ikke noget på denne liste endnu. Når medlemmer af denne liste udgiver nye indlæg, vil de blive vist her.",
"empty_column.mutes": "Du har endnu ikke skjult nogle brugere.", "empty_column.mutes": "Du har endnu ikke skjult nogle brugere.",
"empty_column.notification_requests": "Alt er klar! Der er intet her. Når der modtages nye notifikationer, fremgår de her jævnfør dine indstillinger.", "empty_column.notification_requests": "Alt er klar! Der er intet her. Når der modtages nye notifikationer, fremgår de her jævnfør dine indstillinger.",
@ -424,8 +427,6 @@
"hints.profiles.see_more_followers": "Se flere følgere på {domain}", "hints.profiles.see_more_followers": "Se flere følgere på {domain}",
"hints.profiles.see_more_follows": "Se flere fulgte på {domain}", "hints.profiles.see_more_follows": "Se flere fulgte på {domain}",
"hints.profiles.see_more_posts": "Se flere indlæg på {domain}", "hints.profiles.see_more_posts": "Se flere indlæg på {domain}",
"hints.threads.replies_may_be_missing": "Der kan mangle svar fra andre servere.",
"hints.threads.see_more": "Se flere svar på {domain}",
"home.column_settings.show_quotes": "Vis citater", "home.column_settings.show_quotes": "Vis citater",
"home.column_settings.show_reblogs": "Vis fremhævelser", "home.column_settings.show_reblogs": "Vis fremhævelser",
"home.column_settings.show_replies": "Vis svar", "home.column_settings.show_replies": "Vis svar",
@ -478,7 +479,7 @@
"keyboard_shortcuts.favourites": "Åbn favoritlisten", "keyboard_shortcuts.favourites": "Åbn favoritlisten",
"keyboard_shortcuts.federated": "Åbn fødereret tidslinje", "keyboard_shortcuts.federated": "Åbn fødereret tidslinje",
"keyboard_shortcuts.heading": "Tastaturgenveje", "keyboard_shortcuts.heading": "Tastaturgenveje",
"keyboard_shortcuts.home": "Åbn hjemmetidslinje", "keyboard_shortcuts.home": "Åbn hjem-tidslinje",
"keyboard_shortcuts.hotkey": "Hurtigtast", "keyboard_shortcuts.hotkey": "Hurtigtast",
"keyboard_shortcuts.legend": "Vis dette symbol", "keyboard_shortcuts.legend": "Vis dette symbol",
"keyboard_shortcuts.local": "Åbn lokal tidslinje", "keyboard_shortcuts.local": "Åbn lokal tidslinje",
@ -500,6 +501,8 @@
"keyboard_shortcuts.translate": "for at oversætte et indlæg", "keyboard_shortcuts.translate": "for at oversætte et indlæg",
"keyboard_shortcuts.unfocus": "Fjern fokus fra tekstskrivningsområde/søgning", "keyboard_shortcuts.unfocus": "Fjern fokus fra tekstskrivningsområde/søgning",
"keyboard_shortcuts.up": "Flyt opad på listen", "keyboard_shortcuts.up": "Flyt opad på listen",
"learn_more_link.got_it": "Forstået",
"learn_more_link.learn_more": "Få mere at vide",
"lightbox.close": "Luk", "lightbox.close": "Luk",
"lightbox.next": "Næste", "lightbox.next": "Næste",
"lightbox.previous": "Forrige", "lightbox.previous": "Forrige",
@ -520,7 +523,7 @@
"lists.done": "Færdig", "lists.done": "Færdig",
"lists.edit": "Redigér liste", "lists.edit": "Redigér liste",
"lists.exclusive": "Skjul medlemmer i Hjem", "lists.exclusive": "Skjul medlemmer i Hjem",
"lists.exclusive_hint": "Er nogen er på denne liste, skjul personen i hjemme-feeds for at undgå at se vedkommendes indlæg to gange.", "lists.exclusive_hint": "Hvis nogen er på denne liste, så skjul dem i hjem-feed for at undgå at se deres indlæg to gange.",
"lists.find_users_to_add": "Find brugere at tilføje", "lists.find_users_to_add": "Find brugere at tilføje",
"lists.list_members_count": "{count, plural, one {# medlem} other {# medlemmer}}", "lists.list_members_count": "{count, plural, one {# medlem} other {# medlemmer}}",
"lists.list_name": "Listetitel", "lists.list_name": "Listetitel",
@ -600,6 +603,7 @@
"notification.label.mention": "Omtale", "notification.label.mention": "Omtale",
"notification.label.private_mention": "Privat omtale", "notification.label.private_mention": "Privat omtale",
"notification.label.private_reply": "Privat svar", "notification.label.private_reply": "Privat svar",
"notification.label.quote": "{name} citerede dit indlæg",
"notification.label.reply": "Svar", "notification.label.reply": "Svar",
"notification.mention": "Omtale", "notification.mention": "Omtale",
"notification.mentioned_you": "{name} omtalte dig", "notification.mentioned_you": "{name} omtalte dig",
@ -657,6 +661,7 @@
"notifications.column_settings.mention": "Omtaler:", "notifications.column_settings.mention": "Omtaler:",
"notifications.column_settings.poll": "Afstemningsresultater:", "notifications.column_settings.poll": "Afstemningsresultater:",
"notifications.column_settings.push": "Push-notifikationer", "notifications.column_settings.push": "Push-notifikationer",
"notifications.column_settings.quote": "Citater:",
"notifications.column_settings.reblog": "Fremhævelser:", "notifications.column_settings.reblog": "Fremhævelser:",
"notifications.column_settings.show": "Vis i kolonne", "notifications.column_settings.show": "Vis i kolonne",
"notifications.column_settings.sound": "Afspil lyd", "notifications.column_settings.sound": "Afspil lyd",
@ -794,7 +799,7 @@
"report.thanks.title": "Ønsker ikke at se dette?", "report.thanks.title": "Ønsker ikke at se dette?",
"report.thanks.title_actionable": "Tak for anmeldelsen, der vil blive set nærmere på dette.", "report.thanks.title_actionable": "Tak for anmeldelsen, der vil blive set nærmere på dette.",
"report.unfollow": "Følg ikke længere @{name}", "report.unfollow": "Følg ikke længere @{name}",
"report.unfollow_explanation": "Du følger denne konto. For ikke længere at se vedkommendes indlæg i din hjemmestrøm, kan du stoppe med at følge dem.", "report.unfollow_explanation": "Du følger denne konto. Hvis du ikke længere vil se vedkommendes indlæg i dit hjem-feed, så stop med at følge dem.",
"report_notification.attached_statuses": "{count, plural, one {{count} indlæg} other {{count} indlæg}} vedhæftet", "report_notification.attached_statuses": "{count, plural, one {{count} indlæg} other {{count} indlæg}} vedhæftet",
"report_notification.categories.legal": "Juridisk", "report_notification.categories.legal": "Juridisk",
"report_notification.categories.legal_sentence": "ikke-tilladt indhold", "report_notification.categories.legal_sentence": "ikke-tilladt indhold",
@ -847,6 +852,8 @@
"status.bookmark": "Bogmærk", "status.bookmark": "Bogmærk",
"status.cancel_reblog_private": "Fjern fremhævelse", "status.cancel_reblog_private": "Fjern fremhævelse",
"status.cannot_reblog": "Dette indlæg kan ikke fremhæves", "status.cannot_reblog": "Dette indlæg kan ikke fremhæves",
"status.context.load_new_replies": "Nye svar tilgængelige",
"status.context.loading": "Tjekker for flere svar",
"status.continued_thread": "Fortsat tråd", "status.continued_thread": "Fortsat tråd",
"status.copy": "Kopiér link til indlæg", "status.copy": "Kopiér link til indlæg",
"status.delete": "Slet", "status.delete": "Slet",
@ -873,12 +880,11 @@
"status.open": "Udvid dette indlæg", "status.open": "Udvid dette indlæg",
"status.pin": "Fastgør til profil", "status.pin": "Fastgør til profil",
"status.quote_error.filtered": "Skjult grundet et af filterne", "status.quote_error.filtered": "Skjult grundet et af filterne",
"status.quote_error.not_found": "Dette indlæg kan ikke vises.", "status.quote_error.not_available": "Indlæg utilgængeligt",
"status.quote_error.pending_approval": "Dette indlæg afventer godkendelse fra den oprindelige forfatter.", "status.quote_error.pending_approval": "Afventende indlæg",
"status.quote_error.rejected": "Dette indlæg kan ikke vises, da den oprindelige forfatter ikke tillader citering heraf.", "status.quote_error.pending_approval_popout.body": "Citater delt på tværs af Fediverset kan tage tid at vise, da forskellige servere har forskellige protokoller.",
"status.quote_error.removed": "Dette indlæg er fjernet af forfatteren.", "status.quote_error.pending_approval_popout.title": "Afventende citat? Tag det roligt",
"status.quote_error.unauthorized": "Dette indlæg kan ikke vises, da man ikke har tilladelse til at se det.", "status.quote_post_author": "Citerede et indlæg fra @{name}",
"status.quote_post_author": "Indlæg fra {name}",
"status.read_more": "Læs mere", "status.read_more": "Læs mere",
"status.reblog": "Fremhæv", "status.reblog": "Fremhæv",
"status.reblog_private": "Fremhæv med oprindelig synlighed", "status.reblog_private": "Fremhæv med oprindelig synlighed",
@ -893,6 +899,7 @@
"status.reply": "Besvar", "status.reply": "Besvar",
"status.replyAll": "Svar alle", "status.replyAll": "Svar alle",
"status.report": "Anmeld @{name}", "status.report": "Anmeld @{name}",
"status.revoke_quote": "Fjern mit indlæg fra @{name}'s indlæg",
"status.sensitive_warning": "Følsomt indhold", "status.sensitive_warning": "Følsomt indhold",
"status.share": "Del", "status.share": "Del",
"status.show_less_all": "Vis mindre for alle", "status.show_less_all": "Vis mindre for alle",

View File

@ -6,7 +6,7 @@
"about.domain_blocks.no_reason_available": "Grund unbekannt", "about.domain_blocks.no_reason_available": "Grund unbekannt",
"about.domain_blocks.preamble": "Mastodon erlaubt es dir grundsätzlich, alle Inhalte von allen Nutzer*innen auf allen Servern im Fediverse zu sehen und mit ihnen zu interagieren. Für diesen Server gibt es aber ein paar Ausnahmen.", "about.domain_blocks.preamble": "Mastodon erlaubt es dir grundsätzlich, alle Inhalte von allen Nutzer*innen auf allen Servern im Fediverse zu sehen und mit ihnen zu interagieren. Für diesen Server gibt es aber ein paar Ausnahmen.",
"about.domain_blocks.silenced.explanation": "Standardmäßig werden von diesem Server keine Inhalte oder Profile angezeigt. Du kannst die Profile und Inhalte aber dennoch sehen, wenn du explizit nach diesen suchst oder diesen folgst.", "about.domain_blocks.silenced.explanation": "Standardmäßig werden von diesem Server keine Inhalte oder Profile angezeigt. Du kannst die Profile und Inhalte aber dennoch sehen, wenn du explizit nach diesen suchst oder diesen folgst.",
"about.domain_blocks.silenced.title": "Stummgeschaltet", "about.domain_blocks.silenced.title": "Ausgeblendet",
"about.domain_blocks.suspended.explanation": "Es werden keine Daten von diesem Server verarbeitet, gespeichert oder ausgetauscht, sodass eine Interaktion oder Kommunikation mit Nutzer*innen dieses Servers nicht möglich ist.", "about.domain_blocks.suspended.explanation": "Es werden keine Daten von diesem Server verarbeitet, gespeichert oder ausgetauscht, sodass eine Interaktion oder Kommunikation mit Nutzer*innen dieses Servers nicht möglich ist.",
"about.domain_blocks.suspended.title": "Gesperrt", "about.domain_blocks.suspended.title": "Gesperrt",
"about.language_label": "Sprache", "about.language_label": "Sprache",
@ -63,7 +63,7 @@
"account.mute_short": "Stummschalten", "account.mute_short": "Stummschalten",
"account.muted": "Stummgeschaltet", "account.muted": "Stummgeschaltet",
"account.muting": "Stummgeschaltet", "account.muting": "Stummgeschaltet",
"account.mutual": "Ihr folgt einander", "account.mutual": "Ihr folgt euch",
"account.no_bio": "Keine Beschreibung verfügbar.", "account.no_bio": "Keine Beschreibung verfügbar.",
"account.open_original_page": "Ursprüngliche Seite öffnen", "account.open_original_page": "Ursprüngliche Seite öffnen",
"account.posts": "Beiträge", "account.posts": "Beiträge",
@ -225,7 +225,7 @@
"confirmations.discard_draft.edit.title": "Änderungen an diesem Beitrag verwerfen?", "confirmations.discard_draft.edit.title": "Änderungen an diesem Beitrag verwerfen?",
"confirmations.discard_draft.post.cancel": "Entwurf fortsetzen", "confirmations.discard_draft.post.cancel": "Entwurf fortsetzen",
"confirmations.discard_draft.post.message": "Beim Fortfahren wird der gerade verfasste Beitrag verworfen.", "confirmations.discard_draft.post.message": "Beim Fortfahren wird der gerade verfasste Beitrag verworfen.",
"confirmations.discard_draft.post.title": "Beitragsentwurf verwerfen?", "confirmations.discard_draft.post.title": "Entwurf verwerfen?",
"confirmations.discard_edit_media.confirm": "Verwerfen", "confirmations.discard_edit_media.confirm": "Verwerfen",
"confirmations.discard_edit_media.message": "Du hast Änderungen an der Medienbeschreibung oder -vorschau vorgenommen, die noch nicht gespeichert sind. Trotzdem verwerfen?", "confirmations.discard_edit_media.message": "Du hast Änderungen an der Medienbeschreibung oder -vorschau vorgenommen, die noch nicht gespeichert sind. Trotzdem verwerfen?",
"confirmations.follow_to_list.confirm": "Folgen und zur Liste hinzufügen", "confirmations.follow_to_list.confirm": "Folgen und zur Liste hinzufügen",
@ -245,6 +245,9 @@
"confirmations.remove_from_followers.confirm": "Follower entfernen", "confirmations.remove_from_followers.confirm": "Follower entfernen",
"confirmations.remove_from_followers.message": "{name} wird dir nicht länger folgen. Bist du dir sicher?", "confirmations.remove_from_followers.message": "{name} wird dir nicht länger folgen. Bist du dir sicher?",
"confirmations.remove_from_followers.title": "Follower entfernen?", "confirmations.remove_from_followers.title": "Follower entfernen?",
"confirmations.revoke_quote.confirm": "Beitrag entfernen",
"confirmations.revoke_quote.message": "Diese Aktion kann nicht rückgängig gemacht werden.",
"confirmations.revoke_quote.title": "Beitrag entfernen?",
"confirmations.unfollow.confirm": "Entfolgen", "confirmations.unfollow.confirm": "Entfolgen",
"confirmations.unfollow.message": "Möchtest du {name} wirklich entfolgen?", "confirmations.unfollow.message": "Möchtest du {name} wirklich entfolgen?",
"confirmations.unfollow.title": "Profil entfolgen?", "confirmations.unfollow.title": "Profil entfolgen?",
@ -362,7 +365,7 @@
"filter_modal.select_filter.subtitle": "Einem vorhandenen Filter hinzufügen oder einen neuen erstellen", "filter_modal.select_filter.subtitle": "Einem vorhandenen Filter hinzufügen oder einen neuen erstellen",
"filter_modal.select_filter.title": "Diesen Beitrag filtern", "filter_modal.select_filter.title": "Diesen Beitrag filtern",
"filter_modal.title.status": "Beitrag per Filter ausblenden", "filter_modal.title.status": "Beitrag per Filter ausblenden",
"filter_warning.matches_filter": "Übereinstimmend mit dem Filter „<span>{title}</span>“", "filter_warning.matches_filter": "Ausgeblendet wegen des Filters „<span>{title}</span>“",
"filtered_notifications_banner.pending_requests": "Von {count, plural, =0 {keinem Profil, das dir möglicherweise bekannt ist} one {einem Profil, das dir möglicherweise bekannt ist} other {# Profilen, die dir möglicherweise bekannt sind}}", "filtered_notifications_banner.pending_requests": "Von {count, plural, =0 {keinem Profil, das dir möglicherweise bekannt ist} one {einem Profil, das dir möglicherweise bekannt ist} other {# Profilen, die dir möglicherweise bekannt sind}}",
"filtered_notifications_banner.title": "Gefilterte Benachrichtigungen", "filtered_notifications_banner.title": "Gefilterte Benachrichtigungen",
"firehose.all": "Alle Server", "firehose.all": "Alle Server",
@ -424,8 +427,6 @@
"hints.profiles.see_more_followers": "Weitere Follower auf {domain} ansehen", "hints.profiles.see_more_followers": "Weitere Follower auf {domain} ansehen",
"hints.profiles.see_more_follows": "Weitere gefolgte Profile auf {domain} ansehen", "hints.profiles.see_more_follows": "Weitere gefolgte Profile auf {domain} ansehen",
"hints.profiles.see_more_posts": "Weitere Beiträge auf {domain} ansehen", "hints.profiles.see_more_posts": "Weitere Beiträge auf {domain} ansehen",
"hints.threads.replies_may_be_missing": "Möglicherweise werden nicht alle Antworten von anderen Servern angezeigt.",
"hints.threads.see_more": "Weitere Antworten auf {domain} ansehen",
"home.column_settings.show_quotes": "Zitierte Beiträge anzeigen", "home.column_settings.show_quotes": "Zitierte Beiträge anzeigen",
"home.column_settings.show_reblogs": "Geteilte Beiträge anzeigen", "home.column_settings.show_reblogs": "Geteilte Beiträge anzeigen",
"home.column_settings.show_replies": "Antworten anzeigen", "home.column_settings.show_replies": "Antworten anzeigen",
@ -500,6 +501,8 @@
"keyboard_shortcuts.translate": "Beitrag übersetzen", "keyboard_shortcuts.translate": "Beitrag übersetzen",
"keyboard_shortcuts.unfocus": "Eingabefeld/Suche nicht mehr fokussieren", "keyboard_shortcuts.unfocus": "Eingabefeld/Suche nicht mehr fokussieren",
"keyboard_shortcuts.up": "Ansicht nach oben bewegen", "keyboard_shortcuts.up": "Ansicht nach oben bewegen",
"learn_more_link.got_it": "Verstanden",
"learn_more_link.learn_more": "Mehr erfahren",
"lightbox.close": "Schließen", "lightbox.close": "Schließen",
"lightbox.next": "Vor", "lightbox.next": "Vor",
"lightbox.previous": "Zurück", "lightbox.previous": "Zurück",
@ -600,6 +603,7 @@
"notification.label.mention": "Erwähnung", "notification.label.mention": "Erwähnung",
"notification.label.private_mention": "Private Erwähnung", "notification.label.private_mention": "Private Erwähnung",
"notification.label.private_reply": "Private Antwort", "notification.label.private_reply": "Private Antwort",
"notification.label.quote": "{name} zitierte deinen Beitrag",
"notification.label.reply": "Antwort", "notification.label.reply": "Antwort",
"notification.mention": "Erwähnung", "notification.mention": "Erwähnung",
"notification.mentioned_you": "{name} erwähnte dich", "notification.mentioned_you": "{name} erwähnte dich",
@ -657,6 +661,7 @@
"notifications.column_settings.mention": "Erwähnungen:", "notifications.column_settings.mention": "Erwähnungen:",
"notifications.column_settings.poll": "Umfrageergebnisse:", "notifications.column_settings.poll": "Umfrageergebnisse:",
"notifications.column_settings.push": "Push-Benachrichtigungen", "notifications.column_settings.push": "Push-Benachrichtigungen",
"notifications.column_settings.quote": "Zitate:",
"notifications.column_settings.reblog": "Geteilte Beiträge:", "notifications.column_settings.reblog": "Geteilte Beiträge:",
"notifications.column_settings.show": "In dieser Spalte anzeigen", "notifications.column_settings.show": "In dieser Spalte anzeigen",
"notifications.column_settings.sound": "Ton abspielen", "notifications.column_settings.sound": "Ton abspielen",
@ -847,6 +852,8 @@
"status.bookmark": "Lesezeichen setzen", "status.bookmark": "Lesezeichen setzen",
"status.cancel_reblog_private": "Beitrag nicht mehr teilen", "status.cancel_reblog_private": "Beitrag nicht mehr teilen",
"status.cannot_reblog": "Dieser Beitrag kann nicht geteilt werden", "status.cannot_reblog": "Dieser Beitrag kann nicht geteilt werden",
"status.context.load_new_replies": "Neue Antworten verfügbar",
"status.context.loading": "Weitere Antworten werden abgerufen",
"status.continued_thread": "Fortgeführter Thread", "status.continued_thread": "Fortgeführter Thread",
"status.copy": "Link zum Beitrag kopieren", "status.copy": "Link zum Beitrag kopieren",
"status.delete": "Beitrag löschen", "status.delete": "Beitrag löschen",
@ -873,12 +880,11 @@
"status.open": "Beitrag öffnen", "status.open": "Beitrag öffnen",
"status.pin": "Im Profil anheften", "status.pin": "Im Profil anheften",
"status.quote_error.filtered": "Ausgeblendet wegen eines deiner Filter", "status.quote_error.filtered": "Ausgeblendet wegen eines deiner Filter",
"status.quote_error.not_found": "Dieser Beitrag kann nicht angezeigt werden.", "status.quote_error.not_available": "Beitrag nicht verfügbar",
"status.quote_error.pending_approval": "Dieser Beitrag muss noch durch das ursprüngliche Profil genehmigt werden.", "status.quote_error.pending_approval": "Beitragsveröffentlichung ausstehend",
"status.quote_error.rejected": "Dieser Beitrag kann nicht angezeigt werden, weil das ursprüngliche Profil das Zitieren nicht erlaubt.", "status.quote_error.pending_approval_popout.body": "Zitierte Beiträge, die im Fediverse geteilt werden, benötigen einige Zeit, bis sie überall angezeigt werden, da die verschiedenen Server unterschiedliche Protokolle nutzen.",
"status.quote_error.removed": "Dieser Beitrag wurde durch das Profil entfernt.", "status.quote_error.pending_approval_popout.title": "Zitierter Beitrag noch nicht freigegeben? Immer mit der Ruhe",
"status.quote_error.unauthorized": "Dieser Beitrag kann nicht angezeigt werden, weil du zum Ansehen nicht berechtigt bist.", "status.quote_post_author": "Zitierte einen Beitrag von @{name}",
"status.quote_post_author": "Beitrag von {name}",
"status.read_more": "Gesamten Beitrag anschauen", "status.read_more": "Gesamten Beitrag anschauen",
"status.reblog": "Teilen", "status.reblog": "Teilen",
"status.reblog_private": "Mit der ursprünglichen Zielgruppe teilen", "status.reblog_private": "Mit der ursprünglichen Zielgruppe teilen",
@ -893,6 +899,7 @@
"status.reply": "Antworten", "status.reply": "Antworten",
"status.replyAll": "Allen antworten", "status.replyAll": "Allen antworten",
"status.report": "@{name} melden", "status.report": "@{name} melden",
"status.revoke_quote": "Meinen zitierten Beitrag aus dem Beitrag von @{name} entfernen",
"status.sensitive_warning": "Inhaltswarnung", "status.sensitive_warning": "Inhaltswarnung",
"status.share": "Teilen", "status.share": "Teilen",
"status.show_less_all": "Alles einklappen", "status.show_less_all": "Alles einklappen",

View File

@ -424,8 +424,6 @@
"hints.profiles.see_more_followers": "Δες περισσότερους ακόλουθους στο {domain}", "hints.profiles.see_more_followers": "Δες περισσότερους ακόλουθους στο {domain}",
"hints.profiles.see_more_follows": "Δες περισσότερα άτομα που ακολουθούνται στο {domain}", "hints.profiles.see_more_follows": "Δες περισσότερα άτομα που ακολουθούνται στο {domain}",
"hints.profiles.see_more_posts": "Δες περισσότερες αναρτήσεις στο {domain}", "hints.profiles.see_more_posts": "Δες περισσότερες αναρτήσεις στο {domain}",
"hints.threads.replies_may_be_missing": "Απαντήσεις από άλλους διακομιστές μπορεί να λείπουν.",
"hints.threads.see_more": "Δες περισσότερες απαντήσεις στο {domain}",
"home.column_settings.show_quotes": "Εμφάνιση παραθεμάτων", "home.column_settings.show_quotes": "Εμφάνιση παραθεμάτων",
"home.column_settings.show_reblogs": "Εμφάνιση προωθήσεων", "home.column_settings.show_reblogs": "Εμφάνιση προωθήσεων",
"home.column_settings.show_replies": "Εμφάνιση απαντήσεων", "home.column_settings.show_replies": "Εμφάνιση απαντήσεων",
@ -500,6 +498,8 @@
"keyboard_shortcuts.translate": "για να μεταφραστεί μια ανάρτηση", "keyboard_shortcuts.translate": "για να μεταφραστεί μια ανάρτηση",
"keyboard_shortcuts.unfocus": "Αποεστίαση του πεδίου σύνθεσης/αναζήτησης", "keyboard_shortcuts.unfocus": "Αποεστίαση του πεδίου σύνθεσης/αναζήτησης",
"keyboard_shortcuts.up": "Μετακίνηση προς τα πάνω στη λίστα", "keyboard_shortcuts.up": "Μετακίνηση προς τα πάνω στη λίστα",
"learn_more_link.got_it": "Το κατάλαβα",
"learn_more_link.learn_more": "Μάθε περισσότερα",
"lightbox.close": "Κλείσιμο", "lightbox.close": "Κλείσιμο",
"lightbox.next": "Επόμενο", "lightbox.next": "Επόμενο",
"lightbox.previous": "Προηγούμενο", "lightbox.previous": "Προηγούμενο",
@ -600,6 +600,7 @@
"notification.label.mention": "Επισήμανση", "notification.label.mention": "Επισήμανση",
"notification.label.private_mention": "Ιδιωτική επισήμανση", "notification.label.private_mention": "Ιδιωτική επισήμανση",
"notification.label.private_reply": "Ιδιωτική απάντηση", "notification.label.private_reply": "Ιδιωτική απάντηση",
"notification.label.quote": "Ο/Η {name} έκανε παράθεση της ανάρτησής σου",
"notification.label.reply": "Απάντηση", "notification.label.reply": "Απάντηση",
"notification.mention": "Επισήμανση", "notification.mention": "Επισήμανση",
"notification.mentioned_you": "Ο χρήστης {name} σε επισήμανε", "notification.mentioned_you": "Ο χρήστης {name} σε επισήμανε",
@ -614,7 +615,7 @@
"notification.moderation_warning.action_suspend": "Ο λογαριασμός σου έχει ανασταλεί.", "notification.moderation_warning.action_suspend": "Ο λογαριασμός σου έχει ανασταλεί.",
"notification.own_poll": "Η δημοσκόπησή σου έληξε", "notification.own_poll": "Η δημοσκόπησή σου έληξε",
"notification.poll": "Μία ψηφοφορία στην οποία συμμετείχες έχει τελειώσει", "notification.poll": "Μία ψηφοφορία στην οποία συμμετείχες έχει τελειώσει",
"notification.reblog": "Ο/Η {name} ενίσχυσε τη δημοσίευσή σου", "notification.reblog": "Ο/Η {name} ενίσχυσε την ανάρτηση σου",
"notification.reblog.name_and_others_with_link": "{name} και <a>{count, plural, one {# ακόμη} other {# ακόμη}}</a> ενίσχυσαν την ανάρτησή σου", "notification.reblog.name_and_others_with_link": "{name} και <a>{count, plural, one {# ακόμη} other {# ακόμη}}</a> ενίσχυσαν την ανάρτησή σου",
"notification.relationships_severance_event": "Χάθηκε η σύνδεση με το {name}", "notification.relationships_severance_event": "Χάθηκε η σύνδεση με το {name}",
"notification.relationships_severance_event.account_suspension": "Ένας διαχειριστής από το {from} ανέστειλε το {target}, πράγμα που σημαίνει ότι δεν μπορείς πλέον να λαμβάνεις ενημερώσεις από αυτούς ή να αλληλεπιδράς μαζί τους.", "notification.relationships_severance_event.account_suspension": "Ένας διαχειριστής από το {from} ανέστειλε το {target}, πράγμα που σημαίνει ότι δεν μπορείς πλέον να λαμβάνεις ενημερώσεις από αυτούς ή να αλληλεπιδράς μαζί τους.",
@ -657,6 +658,7 @@
"notifications.column_settings.mention": "Επισημάνσεις:", "notifications.column_settings.mention": "Επισημάνσεις:",
"notifications.column_settings.poll": "Αποτελέσματα δημοσκόπησης:", "notifications.column_settings.poll": "Αποτελέσματα δημοσκόπησης:",
"notifications.column_settings.push": "Ειδοποιήσεις Push", "notifications.column_settings.push": "Ειδοποιήσεις Push",
"notifications.column_settings.quote": "Παραθέσεις:",
"notifications.column_settings.reblog": "Ενισχύσεις:", "notifications.column_settings.reblog": "Ενισχύσεις:",
"notifications.column_settings.show": "Εμφάνισε σε στήλη", "notifications.column_settings.show": "Εμφάνισε σε στήλη",
"notifications.column_settings.sound": "Αναπαραγωγή ήχου", "notifications.column_settings.sound": "Αναπαραγωγή ήχου",
@ -847,6 +849,8 @@
"status.bookmark": "Σελιδοδείκτης", "status.bookmark": "Σελιδοδείκτης",
"status.cancel_reblog_private": "Ακύρωση ενίσχυσης", "status.cancel_reblog_private": "Ακύρωση ενίσχυσης",
"status.cannot_reblog": "Αυτή η ανάρτηση δεν μπορεί να ενισχυθεί", "status.cannot_reblog": "Αυτή η ανάρτηση δεν μπορεί να ενισχυθεί",
"status.context.load_new_replies": "Νέες απαντήσεις διαθέσιμες",
"status.context.loading": "Γίνεται έλεγχος για περισσότερες απαντήσεις",
"status.continued_thread": "Συνεχιζόμενο νήματος", "status.continued_thread": "Συνεχιζόμενο νήματος",
"status.copy": "Αντιγραφή συνδέσμου ανάρτησης", "status.copy": "Αντιγραφή συνδέσμου ανάρτησης",
"status.delete": "Διαγραφή", "status.delete": "Διαγραφή",
@ -873,12 +877,11 @@
"status.open": "Επέκταση ανάρτησης", "status.open": "Επέκταση ανάρτησης",
"status.pin": "Καρφίτσωσε στο προφίλ", "status.pin": "Καρφίτσωσε στο προφίλ",
"status.quote_error.filtered": "Κρυφό λόγω ενός από τα φίλτρα σου", "status.quote_error.filtered": "Κρυφό λόγω ενός από τα φίλτρα σου",
"status.quote_error.not_found": "Αυτή η ανάρτηση δεν μπορεί να εμφανιστεί.", "status.quote_error.not_available": "Ανάρτηση μη διαθέσιμη",
"status.quote_error.pending_approval": "Αυτή η ανάρτηση εκκρεμεί έγκριση από τον αρχικό συντάκτη.", "status.quote_error.pending_approval": "Ανάρτηση σε αναμονή",
"status.quote_error.rejected": "Αυτή η ανάρτηση δεν μπορεί να εμφανιστεί καθώς ο αρχικός συντάκτης δεν επιτρέπει τις παραθέσεις.", "status.quote_error.pending_approval_popout.body": "Οι παραθέσεις που μοιράζονται στο Fediverse μπορεί να χρειαστούν χρόνο για να εμφανιστούν, καθώς διαφορετικοί διακομιστές έχουν διαφορετικά πρωτόκολλα.",
"status.quote_error.removed": "Αυτή η ανάρτηση αφαιρέθηκε από τον συντάκτη της.", "status.quote_error.pending_approval_popout.title": "Παράθεση σε εκκρεμότητα; Μείνετε ψύχραιμοι",
"status.quote_error.unauthorized": "Αυτή η ανάρτηση δεν μπορεί να εμφανιστεί καθώς δεν έχεις εξουσιοδότηση για να τη δεις.", "status.quote_post_author": "Παρατίθεται μια ανάρτηση από @{name}",
"status.quote_post_author": "Ανάρτηση από {name}",
"status.read_more": "Διάβασε περισότερα", "status.read_more": "Διάβασε περισότερα",
"status.reblog": "Ενίσχυση", "status.reblog": "Ενίσχυση",
"status.reblog_private": "Ενίσχυση με αρχική ορατότητα", "status.reblog_private": "Ενίσχυση με αρχική ορατότητα",

View File

@ -424,8 +424,6 @@
"hints.profiles.see_more_followers": "See more followers on {domain}", "hints.profiles.see_more_followers": "See more followers on {domain}",
"hints.profiles.see_more_follows": "See more follows on {domain}", "hints.profiles.see_more_follows": "See more follows on {domain}",
"hints.profiles.see_more_posts": "See more posts on {domain}", "hints.profiles.see_more_posts": "See more posts on {domain}",
"hints.threads.replies_may_be_missing": "Replies from other servers may be missing.",
"hints.threads.see_more": "See more replies on {domain}",
"home.column_settings.show_quotes": "Show quotes", "home.column_settings.show_quotes": "Show quotes",
"home.column_settings.show_reblogs": "Show boosts", "home.column_settings.show_reblogs": "Show boosts",
"home.column_settings.show_replies": "Show replies", "home.column_settings.show_replies": "Show replies",
@ -614,7 +612,7 @@
"notification.moderation_warning.action_suspend": "Your account has been suspended.", "notification.moderation_warning.action_suspend": "Your account has been suspended.",
"notification.own_poll": "Your poll has ended", "notification.own_poll": "Your poll has ended",
"notification.poll": "A poll you voted in has ended", "notification.poll": "A poll you voted in has ended",
"notification.reblog": "{name} boosted your status", "notification.reblog": "{name} boosted your post",
"notification.reblog.name_and_others_with_link": "{name} and <a>{count, plural, one {# other} other {# others}}</a> boosted your post", "notification.reblog.name_and_others_with_link": "{name} and <a>{count, plural, one {# other} other {# others}}</a> boosted your post",
"notification.relationships_severance_event": "Lost connections with {name}", "notification.relationships_severance_event": "Lost connections with {name}",
"notification.relationships_severance_event.account_suspension": "An admin from {from} has suspended {target}, which means you can no longer receive updates from them or interact with them.", "notification.relationships_severance_event.account_suspension": "An admin from {from} has suspended {target}, which means you can no longer receive updates from them or interact with them.",
@ -873,12 +871,6 @@
"status.open": "Expand this post", "status.open": "Expand this post",
"status.pin": "Pin on profile", "status.pin": "Pin on profile",
"status.quote_error.filtered": "Hidden due to one of your filters", "status.quote_error.filtered": "Hidden due to one of your filters",
"status.quote_error.not_found": "This post cannot be displayed.",
"status.quote_error.pending_approval": "This post is pending approval from the original author.",
"status.quote_error.rejected": "This post cannot be displayed as the original author does not allow it to be quoted.",
"status.quote_error.removed": "This post was removed by its author.",
"status.quote_error.unauthorized": "This post cannot be displayed as you are not authorised",
"status.quote_post_author": "Post by {name}",
"status.read_more": "Read more", "status.read_more": "Read more",
"status.reblog": "Boost", "status.reblog": "Boost",
"status.reblog_private": "Boost with original visibility", "status.reblog_private": "Boost with original visibility",

View File

@ -245,6 +245,9 @@
"confirmations.remove_from_followers.confirm": "Remove follower", "confirmations.remove_from_followers.confirm": "Remove follower",
"confirmations.remove_from_followers.message": "{name} will stop following you. Are you sure you want to proceed?", "confirmations.remove_from_followers.message": "{name} will stop following you. Are you sure you want to proceed?",
"confirmations.remove_from_followers.title": "Remove follower?", "confirmations.remove_from_followers.title": "Remove follower?",
"confirmations.revoke_quote.confirm": "Remove post",
"confirmations.revoke_quote.message": "This action cannot be undone.",
"confirmations.revoke_quote.title": "Remove post?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"confirmations.unfollow.title": "Unfollow user?", "confirmations.unfollow.title": "Unfollow user?",
@ -498,6 +501,8 @@
"keyboard_shortcuts.translate": "to translate a post", "keyboard_shortcuts.translate": "to translate a post",
"keyboard_shortcuts.unfocus": "Unfocus compose textarea/search", "keyboard_shortcuts.unfocus": "Unfocus compose textarea/search",
"keyboard_shortcuts.up": "Move up in the list", "keyboard_shortcuts.up": "Move up in the list",
"learn_more_link.got_it": "Got it",
"learn_more_link.learn_more": "Learn more",
"lightbox.close": "Close", "lightbox.close": "Close",
"lightbox.next": "Next", "lightbox.next": "Next",
"lightbox.previous": "Previous", "lightbox.previous": "Previous",
@ -598,6 +603,7 @@
"notification.label.mention": "Mention", "notification.label.mention": "Mention",
"notification.label.private_mention": "Private mention", "notification.label.private_mention": "Private mention",
"notification.label.private_reply": "Private reply", "notification.label.private_reply": "Private reply",
"notification.label.quote": "{name} quoted your post",
"notification.label.reply": "Reply", "notification.label.reply": "Reply",
"notification.mention": "Mention", "notification.mention": "Mention",
"notification.mentioned_you": "{name} mentioned you", "notification.mentioned_you": "{name} mentioned you",
@ -655,6 +661,7 @@
"notifications.column_settings.mention": "Mentions:", "notifications.column_settings.mention": "Mentions:",
"notifications.column_settings.poll": "Poll results:", "notifications.column_settings.poll": "Poll results:",
"notifications.column_settings.push": "Push notifications", "notifications.column_settings.push": "Push notifications",
"notifications.column_settings.quote": "Quotes:",
"notifications.column_settings.reblog": "Boosts:", "notifications.column_settings.reblog": "Boosts:",
"notifications.column_settings.show": "Show in column", "notifications.column_settings.show": "Show in column",
"notifications.column_settings.sound": "Play sound", "notifications.column_settings.sound": "Play sound",
@ -873,12 +880,11 @@
"status.open": "Expand this post", "status.open": "Expand this post",
"status.pin": "Pin on profile", "status.pin": "Pin on profile",
"status.quote_error.filtered": "Hidden due to one of your filters", "status.quote_error.filtered": "Hidden due to one of your filters",
"status.quote_error.not_found": "This post cannot be displayed.", "status.quote_error.not_available": "Post unavailable",
"status.quote_error.pending_approval": "This post is pending approval from the original author.", "status.quote_error.pending_approval": "Post pending",
"status.quote_error.rejected": "This post cannot be displayed as the original author does not allow it to be quoted.", "status.quote_error.pending_approval_popout.body": "Quotes shared across the Fediverse may take time to display, as different servers have different protocols.",
"status.quote_error.removed": "This post was removed by its author.", "status.quote_error.pending_approval_popout.title": "Pending quote? Remain calm",
"status.quote_error.unauthorized": "This post cannot be displayed as you are not authorized to view it.", "status.quote_post_author": "Quoted a post by @{name}",
"status.quote_post_author": "Post by {name}",
"status.read_more": "Read more", "status.read_more": "Read more",
"status.reblog": "Boost", "status.reblog": "Boost",
"status.reblog_private": "Boost with original visibility", "status.reblog_private": "Boost with original visibility",
@ -893,6 +899,7 @@
"status.reply": "Reply", "status.reply": "Reply",
"status.replyAll": "Reply to thread", "status.replyAll": "Reply to thread",
"status.report": "Report @{name}", "status.report": "Report @{name}",
"status.revoke_quote": "Remove my post from @{name}s post",
"status.sensitive_warning": "Sensitive content", "status.sensitive_warning": "Sensitive content",
"status.share": "Share", "status.share": "Share",
"status.show_less_all": "Show less for all", "status.show_less_all": "Show less for all",

View File

@ -1,7 +1,7 @@
{ {
"about.blocks": "Reguligitaj serviloj", "about.blocks": "Reguligitaj serviloj",
"about.contact": "Kontakto:", "about.contact": "Kontakto:",
"about.default_locale": "기본", "about.default_locale": "Defaŭlta",
"about.disclaimer": "Mastodon estas libera, malfermitkoda programo kaj varmarko de la firmao Mastodon gGmbH.", "about.disclaimer": "Mastodon estas libera, malfermitkoda programo kaj varmarko de la firmao Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "Kialo ne disponeblas", "about.domain_blocks.no_reason_available": "Kialo ne disponeblas",
"about.domain_blocks.preamble": "Mastodon ĝenerale rajtigas vidi la enhavojn de uzantoj el aliaj serviloj en la fediverso, kaj komuniki kun ili. Jen la limigoj deciditaj de tiu ĉi servilo mem.", "about.domain_blocks.preamble": "Mastodon ĝenerale rajtigas vidi la enhavojn de uzantoj el aliaj serviloj en la fediverso, kaj komuniki kun ili. Jen la limigoj deciditaj de tiu ĉi servilo mem.",
@ -40,6 +40,7 @@
"account.followers": "Sekvantoj", "account.followers": "Sekvantoj",
"account.followers.empty": "Ankoraŭ neniu sekvas ĉi tiun uzanton.", "account.followers.empty": "Ankoraŭ neniu sekvas ĉi tiun uzanton.",
"account.followers_counter": "{count, plural, one{{counter} sekvanto} other {{counter} sekvantoj}}", "account.followers_counter": "{count, plural, one{{counter} sekvanto} other {{counter} sekvantoj}}",
"account.followers_you_know_counter": "Vi scias {counter}",
"account.following": "Sekvatoj", "account.following": "Sekvatoj",
"account.following_counter": "{count, plural, one {{counter} sekvato} other {{counter} sekvatoj}}", "account.following_counter": "{count, plural, one {{counter} sekvato} other {{counter} sekvatoj}}",
"account.follows.empty": "La uzanto ankoraŭ ne sekvas iun ajn.", "account.follows.empty": "La uzanto ankoraŭ ne sekvas iun ajn.",
@ -215,6 +216,11 @@
"confirmations.delete_list.confirm": "Forigi", "confirmations.delete_list.confirm": "Forigi",
"confirmations.delete_list.message": "Ĉu vi certas, ke vi volas porĉiame forigi ĉi tiun liston?", "confirmations.delete_list.message": "Ĉu vi certas, ke vi volas porĉiame forigi ĉi tiun liston?",
"confirmations.delete_list.title": "Ĉu forigi liston?", "confirmations.delete_list.title": "Ĉu forigi liston?",
"confirmations.discard_draft.confirm": "Forĵetu kaj daŭrigu",
"confirmations.discard_draft.edit.cancel": "Daŭrigi redaktadon",
"confirmations.discard_draft.edit.title": "Ĉu forĵeti ŝanĝojn al via afiŝo?",
"confirmations.discard_draft.post.cancel": "Daŭrigi malneton",
"confirmations.discard_draft.post.title": "Ĉu forĵeti vian malneton?",
"confirmations.discard_edit_media.confirm": "Forĵeti", "confirmations.discard_edit_media.confirm": "Forĵeti",
"confirmations.discard_edit_media.message": "Vi havas nekonservitajn ŝanĝojn de la priskribo aŭ la antaŭvidigo de la vidaŭdaĵo, ĉu vi forĵetu ilin malgraŭe?", "confirmations.discard_edit_media.message": "Vi havas nekonservitajn ŝanĝojn de la priskribo aŭ la antaŭvidigo de la vidaŭdaĵo, ĉu vi forĵetu ilin malgraŭe?",
"confirmations.follow_to_list.confirm": "Sekvi kaj aldoni al listo", "confirmations.follow_to_list.confirm": "Sekvi kaj aldoni al listo",
@ -234,6 +240,9 @@
"confirmations.remove_from_followers.confirm": "Forigi sekvanton", "confirmations.remove_from_followers.confirm": "Forigi sekvanton",
"confirmations.remove_from_followers.message": "{name} ne plu sekvos vin. Ĉu vi certas ke vi volas daŭri?", "confirmations.remove_from_followers.message": "{name} ne plu sekvos vin. Ĉu vi certas ke vi volas daŭri?",
"confirmations.remove_from_followers.title": "Forigi sekvanton?", "confirmations.remove_from_followers.title": "Forigi sekvanton?",
"confirmations.revoke_quote.confirm": "Forigi afiŝon",
"confirmations.revoke_quote.message": "Ĉi tiu ago ne povas esti malfarita.",
"confirmations.revoke_quote.title": "Ĉu forigi afiŝon?",
"confirmations.unfollow.confirm": "Ne plu sekvi", "confirmations.unfollow.confirm": "Ne plu sekvi",
"confirmations.unfollow.message": "Ĉu vi certas, ke vi volas ĉesi sekvi {name}?", "confirmations.unfollow.message": "Ĉu vi certas, ke vi volas ĉesi sekvi {name}?",
"confirmations.unfollow.title": "Ĉu ĉesi sekvi uzanton?", "confirmations.unfollow.title": "Ĉu ĉesi sekvi uzanton?",
@ -331,6 +340,7 @@
"featured_carousel.next": "Antaŭen", "featured_carousel.next": "Antaŭen",
"featured_carousel.post": "Afiŝi", "featured_carousel.post": "Afiŝi",
"featured_carousel.previous": "Malantaŭen", "featured_carousel.previous": "Malantaŭen",
"featured_carousel.slide": "{index} de {total}",
"filter_modal.added.context_mismatch_explanation": "Ĉi tiu filtrilkategorio ne kongruas kun la kunteksto en kiu vi akcesis ĉi tiun afiŝon. Se vi volas ke la afiŝo estas ankaŭ filtrita en ĉi tiu kunteksto, vi devus redakti la filtrilon.", "filter_modal.added.context_mismatch_explanation": "Ĉi tiu filtrilkategorio ne kongruas kun la kunteksto en kiu vi akcesis ĉi tiun afiŝon. Se vi volas ke la afiŝo estas ankaŭ filtrita en ĉi tiu kunteksto, vi devus redakti la filtrilon.",
"filter_modal.added.context_mismatch_title": "Ne kongruas la kunteksto!", "filter_modal.added.context_mismatch_title": "Ne kongruas la kunteksto!",
"filter_modal.added.expired_explanation": "Ĉi tiu filtrilkategorio eksvalidiĝis, vu bezonos ŝanĝi la eksvaliddaton por ĝi.", "filter_modal.added.expired_explanation": "Ĉi tiu filtrilkategorio eksvalidiĝis, vu bezonos ŝanĝi la eksvaliddaton por ĝi.",
@ -409,8 +419,7 @@
"hints.profiles.see_more_followers": "Vidi pli da sekvantoj sur {domain}", "hints.profiles.see_more_followers": "Vidi pli da sekvantoj sur {domain}",
"hints.profiles.see_more_follows": "Vidi pli da sekvatoj sur {domain}", "hints.profiles.see_more_follows": "Vidi pli da sekvatoj sur {domain}",
"hints.profiles.see_more_posts": "Vidi pli da afiŝoj sur {domain}", "hints.profiles.see_more_posts": "Vidi pli da afiŝoj sur {domain}",
"hints.threads.replies_may_be_missing": "Respondoj de aliaj serviloj eble mankas.", "home.column_settings.show_quotes": "Montri citaĵojn",
"hints.threads.see_more": "Vidi pli da respondoj sur {domain}",
"home.column_settings.show_reblogs": "Montri diskonigojn", "home.column_settings.show_reblogs": "Montri diskonigojn",
"home.column_settings.show_replies": "Montri respondojn", "home.column_settings.show_replies": "Montri respondojn",
"home.hide_announcements": "Kaŝi la anoncojn", "home.hide_announcements": "Kaŝi la anoncojn",
@ -484,6 +493,8 @@
"keyboard_shortcuts.translate": "Traduki afiŝon", "keyboard_shortcuts.translate": "Traduki afiŝon",
"keyboard_shortcuts.unfocus": "Senfokusigi verki tekstareon/serĉon", "keyboard_shortcuts.unfocus": "Senfokusigi verki tekstareon/serĉon",
"keyboard_shortcuts.up": "Movu supren en la listo", "keyboard_shortcuts.up": "Movu supren en la listo",
"learn_more_link.got_it": "Komprenite",
"learn_more_link.learn_more": "Lernu pli",
"lightbox.close": "Fermi", "lightbox.close": "Fermi",
"lightbox.next": "Antaŭen", "lightbox.next": "Antaŭen",
"lightbox.previous": "Malantaŭen", "lightbox.previous": "Malantaŭen",
@ -533,8 +544,10 @@
"mute_modal.you_wont_see_mentions": "Vi ne vidos afiŝojn, kiuj mencias ilin.", "mute_modal.you_wont_see_mentions": "Vi ne vidos afiŝojn, kiuj mencias ilin.",
"mute_modal.you_wont_see_posts": "Ili ankoraŭ povas vidi viajn afiŝojn, sed vi ne vidos iliajn.", "mute_modal.you_wont_see_posts": "Ili ankoraŭ povas vidi viajn afiŝojn, sed vi ne vidos iliajn.",
"navigation_bar.about": "Pri", "navigation_bar.about": "Pri",
"navigation_bar.account_settings": "Pasvorto kaj sekureco",
"navigation_bar.administration": "Administrado", "navigation_bar.administration": "Administrado",
"navigation_bar.advanced_interface": "Malfermi altnivelan retpaĝan interfacon", "navigation_bar.advanced_interface": "Malfermi altnivelan retpaĝan interfacon",
"navigation_bar.automated_deletion": "Aŭtomata forigo de afiŝoj",
"navigation_bar.blocks": "Blokitaj uzantoj", "navigation_bar.blocks": "Blokitaj uzantoj",
"navigation_bar.bookmarks": "Legosignoj", "navigation_bar.bookmarks": "Legosignoj",
"navigation_bar.direct": "Privataj mencioj", "navigation_bar.direct": "Privataj mencioj",
@ -544,6 +557,7 @@
"navigation_bar.follow_requests": "Petoj de sekvado", "navigation_bar.follow_requests": "Petoj de sekvado",
"navigation_bar.followed_tags": "Sekvataj kradvortoj", "navigation_bar.followed_tags": "Sekvataj kradvortoj",
"navigation_bar.follows_and_followers": "Sekvatoj kaj sekvantoj", "navigation_bar.follows_and_followers": "Sekvatoj kaj sekvantoj",
"navigation_bar.import_export": "Importo kaj eksporto",
"navigation_bar.lists": "Listoj", "navigation_bar.lists": "Listoj",
"navigation_bar.logout": "Elsaluti", "navigation_bar.logout": "Elsaluti",
"navigation_bar.moderation": "Modereco", "navigation_bar.moderation": "Modereco",
@ -551,6 +565,7 @@
"navigation_bar.mutes": "Silentigitaj uzantoj", "navigation_bar.mutes": "Silentigitaj uzantoj",
"navigation_bar.opened_in_classic_interface": "Afiŝoj, kontoj, kaj aliaj specifaj paĝoj kiuj estas malfermititaj defaulta en la klasika reta interfaco.", "navigation_bar.opened_in_classic_interface": "Afiŝoj, kontoj, kaj aliaj specifaj paĝoj kiuj estas malfermititaj defaulta en la klasika reta interfaco.",
"navigation_bar.preferences": "Preferoj", "navigation_bar.preferences": "Preferoj",
"navigation_bar.privacy_and_reach": "Privateco kaj atingo",
"navigation_bar.search": "Serĉi", "navigation_bar.search": "Serĉi",
"not_signed_in_indicator.not_signed_in": "Necesas saluti por aliri tiun rimedon.", "not_signed_in_indicator.not_signed_in": "Necesas saluti por aliri tiun rimedon.",
"notification.admin.report": "{name} raportis {target}", "notification.admin.report": "{name} raportis {target}",
@ -573,6 +588,7 @@
"notification.label.mention": "Mencii", "notification.label.mention": "Mencii",
"notification.label.private_mention": "Privata mencio", "notification.label.private_mention": "Privata mencio",
"notification.label.private_reply": "Privata respondo", "notification.label.private_reply": "Privata respondo",
"notification.label.quote": "{name} citis vian afiŝon",
"notification.label.reply": "Respondi", "notification.label.reply": "Respondi",
"notification.mention": "Mencii", "notification.mention": "Mencii",
"notification.mentioned_you": "{name} menciis vin", "notification.mentioned_you": "{name} menciis vin",
@ -630,6 +646,7 @@
"notifications.column_settings.mention": "Mencioj:", "notifications.column_settings.mention": "Mencioj:",
"notifications.column_settings.poll": "Balotenketaj rezultoj:", "notifications.column_settings.poll": "Balotenketaj rezultoj:",
"notifications.column_settings.push": "Puŝsciigoj", "notifications.column_settings.push": "Puŝsciigoj",
"notifications.column_settings.quote": "Citaĵoj:",
"notifications.column_settings.reblog": "Diskonigoj:", "notifications.column_settings.reblog": "Diskonigoj:",
"notifications.column_settings.show": "Montri en kolumno", "notifications.column_settings.show": "Montri en kolumno",
"notifications.column_settings.sound": "Eligi sonon", "notifications.column_settings.sound": "Eligi sonon",
@ -787,7 +804,7 @@
"search.quick_action.open_url": "Malfermi URL en Mastodono", "search.quick_action.open_url": "Malfermi URL en Mastodono",
"search.quick_action.status_search": "Afiŝoj kiuj konformas kun {x}", "search.quick_action.status_search": "Afiŝoj kiuj konformas kun {x}",
"search.search_or_paste": "Serĉu aŭ algluu URL-on", "search.search_or_paste": "Serĉu aŭ algluu URL-on",
"search_popout.full_text_search_disabled_message": "Ne havebla sur {domain}.", "search_popout.full_text_search_disabled_message": "Ne disponebla sur {domain}.",
"search_popout.full_text_search_logged_out_message": "Disponebla nur kiam ensalutinte.", "search_popout.full_text_search_logged_out_message": "Disponebla nur kiam ensalutinte.",
"search_popout.language_code": "ISO-lingva kodo", "search_popout.language_code": "ISO-lingva kodo",
"search_popout.options": "Serĉaj opcioj", "search_popout.options": "Serĉaj opcioj",
@ -820,6 +837,8 @@
"status.bookmark": "Aldoni al la legosignoj", "status.bookmark": "Aldoni al la legosignoj",
"status.cancel_reblog_private": "Ne plu diskonigi", "status.cancel_reblog_private": "Ne plu diskonigi",
"status.cannot_reblog": "Ĉi tiun afiŝon ne eblas diskonigi", "status.cannot_reblog": "Ĉi tiun afiŝon ne eblas diskonigi",
"status.context.load_new_replies": "Disponeblaj novaj respondoj",
"status.context.loading": "Serĉante pliajn respondojn",
"status.continued_thread": "Daŭrigis fadenon", "status.continued_thread": "Daŭrigis fadenon",
"status.copy": "Kopii la ligilon al la afiŝo", "status.copy": "Kopii la ligilon al la afiŝo",
"status.delete": "Forigi", "status.delete": "Forigi",
@ -845,6 +864,9 @@
"status.mute_conversation": "Silentigi konversacion", "status.mute_conversation": "Silentigi konversacion",
"status.open": "Pligrandigu ĉi tiun afiŝon", "status.open": "Pligrandigu ĉi tiun afiŝon",
"status.pin": "Alpingli al la profilo", "status.pin": "Alpingli al la profilo",
"status.quote_error.not_available": "Afiŝo ne disponebla",
"status.quote_error.pending_approval": "Pritraktata afiŝo",
"status.quote_error.pending_approval_popout.title": "Ĉu pritraktata citaĵo? Restu trankvila",
"status.read_more": "Legi pli", "status.read_more": "Legi pli",
"status.reblog": "Diskonigi", "status.reblog": "Diskonigi",
"status.reblog_private": "Diskonigi kun la sama videbleco", "status.reblog_private": "Diskonigi kun la sama videbleco",
@ -876,6 +898,7 @@
"tabs_bar.home": "Hejmo", "tabs_bar.home": "Hejmo",
"tabs_bar.menu": "Menuo", "tabs_bar.menu": "Menuo",
"tabs_bar.notifications": "Sciigoj", "tabs_bar.notifications": "Sciigoj",
"tabs_bar.publish": "Nova afiŝo",
"tabs_bar.search": "Serĉi", "tabs_bar.search": "Serĉi",
"terms_of_service.effective_as_of": "Ĝi ekvalidas de {date}", "terms_of_service.effective_as_of": "Ĝi ekvalidas de {date}",
"terms_of_service.title": "Kondiĉoj de uzado", "terms_of_service.title": "Kondiĉoj de uzado",

View File

@ -245,6 +245,9 @@
"confirmations.remove_from_followers.confirm": "Quitar seguidor", "confirmations.remove_from_followers.confirm": "Quitar seguidor",
"confirmations.remove_from_followers.message": "{name} dejará de seguirte. ¿Estás seguro de que querés continuar?", "confirmations.remove_from_followers.message": "{name} dejará de seguirte. ¿Estás seguro de que querés continuar?",
"confirmations.remove_from_followers.title": "¿Quitar seguidor?", "confirmations.remove_from_followers.title": "¿Quitar seguidor?",
"confirmations.revoke_quote.confirm": "Eliminar mensaje",
"confirmations.revoke_quote.message": "Esta acción no se puede deshacer.",
"confirmations.revoke_quote.title": "¿Eliminar mensaje?",
"confirmations.unfollow.confirm": "Dejar de seguir", "confirmations.unfollow.confirm": "Dejar de seguir",
"confirmations.unfollow.message": "¿Estás seguro que querés dejar de seguir a {name}?", "confirmations.unfollow.message": "¿Estás seguro que querés dejar de seguir a {name}?",
"confirmations.unfollow.title": "¿Dejar de seguir al usuario?", "confirmations.unfollow.title": "¿Dejar de seguir al usuario?",
@ -424,8 +427,6 @@
"hints.profiles.see_more_followers": "Ver más seguidores en {domain}", "hints.profiles.see_more_followers": "Ver más seguidores en {domain}",
"hints.profiles.see_more_follows": "Ver más seguimientos en {domain}", "hints.profiles.see_more_follows": "Ver más seguimientos en {domain}",
"hints.profiles.see_more_posts": "Ver más mensajes en {domain}", "hints.profiles.see_more_posts": "Ver más mensajes en {domain}",
"hints.threads.replies_may_be_missing": "Es posible que falten respuestas de otros servidores.",
"hints.threads.see_more": "Ver más respuestas en {domain}",
"home.column_settings.show_quotes": "Mostrar citas", "home.column_settings.show_quotes": "Mostrar citas",
"home.column_settings.show_reblogs": "Mostrar adhesiones", "home.column_settings.show_reblogs": "Mostrar adhesiones",
"home.column_settings.show_replies": "Mostrar respuestas", "home.column_settings.show_replies": "Mostrar respuestas",
@ -500,6 +501,8 @@
"keyboard_shortcuts.translate": "para traducir un mensaje", "keyboard_shortcuts.translate": "para traducir un mensaje",
"keyboard_shortcuts.unfocus": "Quitar el foco del área de texto de redacción o de búsqueda", "keyboard_shortcuts.unfocus": "Quitar el foco del área de texto de redacción o de búsqueda",
"keyboard_shortcuts.up": "Subir en la lista", "keyboard_shortcuts.up": "Subir en la lista",
"learn_more_link.got_it": "Entendido",
"learn_more_link.learn_more": "Aprendé más",
"lightbox.close": "Cerrar", "lightbox.close": "Cerrar",
"lightbox.next": "Siguiente", "lightbox.next": "Siguiente",
"lightbox.previous": "Anterior", "lightbox.previous": "Anterior",
@ -600,6 +603,7 @@
"notification.label.mention": "Mención", "notification.label.mention": "Mención",
"notification.label.private_mention": "Mención privada", "notification.label.private_mention": "Mención privada",
"notification.label.private_reply": "Respuesta privada", "notification.label.private_reply": "Respuesta privada",
"notification.label.quote": "{name} citó tu mensaje",
"notification.label.reply": "Respuesta", "notification.label.reply": "Respuesta",
"notification.mention": "Mención", "notification.mention": "Mención",
"notification.mentioned_you": "{name} te mencionó", "notification.mentioned_you": "{name} te mencionó",
@ -657,6 +661,7 @@
"notifications.column_settings.mention": "Menciones:", "notifications.column_settings.mention": "Menciones:",
"notifications.column_settings.poll": "Resultados de la encuesta:", "notifications.column_settings.poll": "Resultados de la encuesta:",
"notifications.column_settings.push": "Notificaciones push", "notifications.column_settings.push": "Notificaciones push",
"notifications.column_settings.quote": "Citas:",
"notifications.column_settings.reblog": "Adhesiones:", "notifications.column_settings.reblog": "Adhesiones:",
"notifications.column_settings.show": "Mostrar en columna", "notifications.column_settings.show": "Mostrar en columna",
"notifications.column_settings.sound": "Reproducir sonido", "notifications.column_settings.sound": "Reproducir sonido",
@ -847,6 +852,8 @@
"status.bookmark": "Marcar", "status.bookmark": "Marcar",
"status.cancel_reblog_private": "Quitar adhesión", "status.cancel_reblog_private": "Quitar adhesión",
"status.cannot_reblog": "No se puede adherir a este mensaje", "status.cannot_reblog": "No se puede adherir a este mensaje",
"status.context.load_new_replies": "Hay nuevas respuestas",
"status.context.loading": "Buscando más respuestas",
"status.continued_thread": "Continuación de hilo", "status.continued_thread": "Continuación de hilo",
"status.copy": "Copiar enlace al mensaje", "status.copy": "Copiar enlace al mensaje",
"status.delete": "Eliminar", "status.delete": "Eliminar",
@ -873,12 +880,11 @@
"status.open": "Expandir este mensaje", "status.open": "Expandir este mensaje",
"status.pin": "Fijar en el perfil", "status.pin": "Fijar en el perfil",
"status.quote_error.filtered": "Oculto debido a uno de tus filtros", "status.quote_error.filtered": "Oculto debido a uno de tus filtros",
"status.quote_error.not_found": "No se puede mostrar este mensaje.", "status.quote_error.not_available": "Mensaje no disponible",
"status.quote_error.pending_approval": "Este mensaje está pendiente de aprobación del autor original.", "status.quote_error.pending_approval": "Mensaje pendiente",
"status.quote_error.rejected": "No se puede mostrar este mensaje, ya que el autor original no permite que se cite.", "status.quote_error.pending_approval_popout.body": "Las citas compartidas a través del Fediverso pueden tardar en mostrarse, ya que diferentes servidores tienen diferentes protocolos.",
"status.quote_error.removed": "Este mensaje fue eliminado por su autor.", "status.quote_error.pending_approval_popout.title": "¿Cita pendiente? Esperá un momento",
"status.quote_error.unauthorized": "No se puede mostrar este mensaje, ya que no tenés autorización para verlo.", "status.quote_post_author": "Se citó un mensaje de @{name}",
"status.quote_post_author": "Mensaje de @{name}",
"status.read_more": "Leé más", "status.read_more": "Leé más",
"status.reblog": "Adherir", "status.reblog": "Adherir",
"status.reblog_private": "Adherir a la audiencia original", "status.reblog_private": "Adherir a la audiencia original",
@ -893,6 +899,7 @@
"status.reply": "Responder", "status.reply": "Responder",
"status.replyAll": "Responder al hilo", "status.replyAll": "Responder al hilo",
"status.report": "Denunciar a @{name}", "status.report": "Denunciar a @{name}",
"status.revoke_quote": "Eliminar mi mensaje de la cita de @{name}",
"status.sensitive_warning": "Contenido sensible", "status.sensitive_warning": "Contenido sensible",
"status.share": "Compartir", "status.share": "Compartir",
"status.show_less_all": "Mostrar menos para todo", "status.show_less_all": "Mostrar menos para todo",

View File

@ -245,6 +245,9 @@
"confirmations.remove_from_followers.confirm": "Eliminar seguidor", "confirmations.remove_from_followers.confirm": "Eliminar seguidor",
"confirmations.remove_from_followers.message": "{name} dejará de seguirte. ¿Estás seguro de que quieres continuar?", "confirmations.remove_from_followers.message": "{name} dejará de seguirte. ¿Estás seguro de que quieres continuar?",
"confirmations.remove_from_followers.title": "¿Eliminar seguidor?", "confirmations.remove_from_followers.title": "¿Eliminar seguidor?",
"confirmations.revoke_quote.confirm": "Eliminar publicación",
"confirmations.revoke_quote.message": "Esta acción no se puede deshacer.",
"confirmations.revoke_quote.title": "¿Deseas eliminar la publicación?",
"confirmations.unfollow.confirm": "Dejar de seguir", "confirmations.unfollow.confirm": "Dejar de seguir",
"confirmations.unfollow.message": "¿Estás seguro de que quieres dejar de seguir a {name}?", "confirmations.unfollow.message": "¿Estás seguro de que quieres dejar de seguir a {name}?",
"confirmations.unfollow.title": "¿Dejar de seguir al usuario?", "confirmations.unfollow.title": "¿Dejar de seguir al usuario?",
@ -424,8 +427,6 @@
"hints.profiles.see_more_followers": "Ver más seguidores en {domain}", "hints.profiles.see_more_followers": "Ver más seguidores en {domain}",
"hints.profiles.see_more_follows": "Ver más perfiles seguidos en {domain}", "hints.profiles.see_more_follows": "Ver más perfiles seguidos en {domain}",
"hints.profiles.see_more_posts": "Ver más publicaciones en {domain}", "hints.profiles.see_more_posts": "Ver más publicaciones en {domain}",
"hints.threads.replies_may_be_missing": "Puede que no se muestren algunas respuestas de otros servidores.",
"hints.threads.see_more": "Ver más respuestas en {domain}",
"home.column_settings.show_quotes": "Mostrar citas", "home.column_settings.show_quotes": "Mostrar citas",
"home.column_settings.show_reblogs": "Mostrar impulsos", "home.column_settings.show_reblogs": "Mostrar impulsos",
"home.column_settings.show_replies": "Mostrar respuestas", "home.column_settings.show_replies": "Mostrar respuestas",
@ -500,6 +501,8 @@
"keyboard_shortcuts.translate": "para traducir una publicación", "keyboard_shortcuts.translate": "para traducir una publicación",
"keyboard_shortcuts.unfocus": "Desenfocar área de redacción/búsqueda", "keyboard_shortcuts.unfocus": "Desenfocar área de redacción/búsqueda",
"keyboard_shortcuts.up": "Ascender en la lista", "keyboard_shortcuts.up": "Ascender en la lista",
"learn_more_link.got_it": "Entendido",
"learn_more_link.learn_more": "Más información",
"lightbox.close": "Cerrar", "lightbox.close": "Cerrar",
"lightbox.next": "Siguiente", "lightbox.next": "Siguiente",
"lightbox.previous": "Anterior", "lightbox.previous": "Anterior",
@ -600,6 +603,7 @@
"notification.label.mention": "Mención", "notification.label.mention": "Mención",
"notification.label.private_mention": "Mención privada", "notification.label.private_mention": "Mención privada",
"notification.label.private_reply": "Respuesta privada", "notification.label.private_reply": "Respuesta privada",
"notification.label.quote": "{name} citó tu publicación",
"notification.label.reply": "Respuesta", "notification.label.reply": "Respuesta",
"notification.mention": "Mención", "notification.mention": "Mención",
"notification.mentioned_you": "{name} te mencionó", "notification.mentioned_you": "{name} te mencionó",
@ -657,6 +661,7 @@
"notifications.column_settings.mention": "Menciones:", "notifications.column_settings.mention": "Menciones:",
"notifications.column_settings.poll": "Resultados de la votación:", "notifications.column_settings.poll": "Resultados de la votación:",
"notifications.column_settings.push": "Notificaciones push", "notifications.column_settings.push": "Notificaciones push",
"notifications.column_settings.quote": "Citas:",
"notifications.column_settings.reblog": "Impulsos:", "notifications.column_settings.reblog": "Impulsos:",
"notifications.column_settings.show": "Mostrar en columna", "notifications.column_settings.show": "Mostrar en columna",
"notifications.column_settings.sound": "Reproducir sonido", "notifications.column_settings.sound": "Reproducir sonido",
@ -847,6 +852,8 @@
"status.bookmark": "Añadir marcador", "status.bookmark": "Añadir marcador",
"status.cancel_reblog_private": "Deshacer impulso", "status.cancel_reblog_private": "Deshacer impulso",
"status.cannot_reblog": "Esta publicación no puede ser impulsada", "status.cannot_reblog": "Esta publicación no puede ser impulsada",
"status.context.load_new_replies": "Nuevas respuestas disponibles",
"status.context.loading": "Comprobando si hay más respuestas",
"status.continued_thread": "Hilo continuado", "status.continued_thread": "Hilo continuado",
"status.copy": "Copiar enlace al estado", "status.copy": "Copiar enlace al estado",
"status.delete": "Borrar", "status.delete": "Borrar",
@ -873,12 +880,11 @@
"status.open": "Expandir estado", "status.open": "Expandir estado",
"status.pin": "Fijar", "status.pin": "Fijar",
"status.quote_error.filtered": "Oculto debido a uno de tus filtros", "status.quote_error.filtered": "Oculto debido a uno de tus filtros",
"status.quote_error.not_found": "No se puede mostrar esta publicación.", "status.quote_error.not_available": "Publicación no disponible",
"status.quote_error.pending_approval": "Esta publicación está pendiente de aprobación del autor original.", "status.quote_error.pending_approval": "Publicación pendiente",
"status.quote_error.rejected": "No se puede mostrar esta publicación, puesto que el autor original no permite que sea citado.", "status.quote_error.pending_approval_popout.body": "Las citas compartidas en el Fediverso pueden tardar en mostrarse, ya que cada servidor tiene un protocolo diferente.",
"status.quote_error.removed": "Esta publicación fue eliminada por su autor.", "status.quote_error.pending_approval_popout.title": "¿Cita pendiente? Mantén la calma",
"status.quote_error.unauthorized": "No se puede mostrar esta publicación, puesto que no estás autorizado a verla.", "status.quote_post_author": "Ha citado una publicación de @{name}",
"status.quote_post_author": "Publicado por {name}",
"status.read_more": "Leer más", "status.read_more": "Leer más",
"status.reblog": "Impulsar", "status.reblog": "Impulsar",
"status.reblog_private": "Implusar a la audiencia original", "status.reblog_private": "Implusar a la audiencia original",
@ -893,6 +899,7 @@
"status.reply": "Responder", "status.reply": "Responder",
"status.replyAll": "Responder al hilo", "status.replyAll": "Responder al hilo",
"status.report": "Reportar @{name}", "status.report": "Reportar @{name}",
"status.revoke_quote": "Eliminar mi publicación de la cita de @{name}",
"status.sensitive_warning": "Contenido sensible", "status.sensitive_warning": "Contenido sensible",
"status.share": "Compartir", "status.share": "Compartir",
"status.show_less_all": "Mostrar menos para todo", "status.show_less_all": "Mostrar menos para todo",

View File

@ -245,6 +245,9 @@
"confirmations.remove_from_followers.confirm": "Eliminar seguidor", "confirmations.remove_from_followers.confirm": "Eliminar seguidor",
"confirmations.remove_from_followers.message": "{name} dejará de seguirte. ¿Estás seguro de que quieres continuar?", "confirmations.remove_from_followers.message": "{name} dejará de seguirte. ¿Estás seguro de que quieres continuar?",
"confirmations.remove_from_followers.title": "¿Eliminar seguidor?", "confirmations.remove_from_followers.title": "¿Eliminar seguidor?",
"confirmations.revoke_quote.confirm": "Eliminar publicación",
"confirmations.revoke_quote.message": "Esta acción no tiene vuelta atrás.",
"confirmations.revoke_quote.title": "¿Eliminar la publicación?",
"confirmations.unfollow.confirm": "Dejar de seguir", "confirmations.unfollow.confirm": "Dejar de seguir",
"confirmations.unfollow.message": "¿Seguro que quieres dejar de seguir a {name}?", "confirmations.unfollow.message": "¿Seguro que quieres dejar de seguir a {name}?",
"confirmations.unfollow.title": "¿Dejar de seguir al usuario?", "confirmations.unfollow.title": "¿Dejar de seguir al usuario?",
@ -424,8 +427,6 @@
"hints.profiles.see_more_followers": "Ver más seguidores en {domain}", "hints.profiles.see_more_followers": "Ver más seguidores en {domain}",
"hints.profiles.see_more_follows": "Ver más perfiles seguidos en {domain}", "hints.profiles.see_more_follows": "Ver más perfiles seguidos en {domain}",
"hints.profiles.see_more_posts": "Ver más publicaciones en {domain}", "hints.profiles.see_more_posts": "Ver más publicaciones en {domain}",
"hints.threads.replies_may_be_missing": "Puede que no se muestren algunas respuestas de otros servidores.",
"hints.threads.see_more": "Ver más respuestas en {domain}",
"home.column_settings.show_quotes": "Mostrar citas", "home.column_settings.show_quotes": "Mostrar citas",
"home.column_settings.show_reblogs": "Mostrar impulsos", "home.column_settings.show_reblogs": "Mostrar impulsos",
"home.column_settings.show_replies": "Mostrar respuestas", "home.column_settings.show_replies": "Mostrar respuestas",
@ -500,6 +501,8 @@
"keyboard_shortcuts.translate": "para traducir una publicación", "keyboard_shortcuts.translate": "para traducir una publicación",
"keyboard_shortcuts.unfocus": "Quitar el foco de la caja de redacción/búsqueda", "keyboard_shortcuts.unfocus": "Quitar el foco de la caja de redacción/búsqueda",
"keyboard_shortcuts.up": "Moverse hacia arriba en la lista", "keyboard_shortcuts.up": "Moverse hacia arriba en la lista",
"learn_more_link.got_it": "Entendido",
"learn_more_link.learn_more": "Más información",
"lightbox.close": "Cerrar", "lightbox.close": "Cerrar",
"lightbox.next": "Siguiente", "lightbox.next": "Siguiente",
"lightbox.previous": "Anterior", "lightbox.previous": "Anterior",
@ -600,6 +603,7 @@
"notification.label.mention": "Mención", "notification.label.mention": "Mención",
"notification.label.private_mention": "Mención privada", "notification.label.private_mention": "Mención privada",
"notification.label.private_reply": "Respuesta privada", "notification.label.private_reply": "Respuesta privada",
"notification.label.quote": "{name} citó tu publicación",
"notification.label.reply": "Respuesta", "notification.label.reply": "Respuesta",
"notification.mention": "Mención", "notification.mention": "Mención",
"notification.mentioned_you": "{name} te ha mencionado", "notification.mentioned_you": "{name} te ha mencionado",
@ -657,6 +661,7 @@
"notifications.column_settings.mention": "Menciones:", "notifications.column_settings.mention": "Menciones:",
"notifications.column_settings.poll": "Resultados de la votación:", "notifications.column_settings.poll": "Resultados de la votación:",
"notifications.column_settings.push": "Notificaciones push", "notifications.column_settings.push": "Notificaciones push",
"notifications.column_settings.quote": "Citas:",
"notifications.column_settings.reblog": "Impulsos:", "notifications.column_settings.reblog": "Impulsos:",
"notifications.column_settings.show": "Mostrar en columna", "notifications.column_settings.show": "Mostrar en columna",
"notifications.column_settings.sound": "Reproducir sonido", "notifications.column_settings.sound": "Reproducir sonido",
@ -847,6 +852,8 @@
"status.bookmark": "Añadir marcador", "status.bookmark": "Añadir marcador",
"status.cancel_reblog_private": "Deshacer impulso", "status.cancel_reblog_private": "Deshacer impulso",
"status.cannot_reblog": "Esta publicación no se puede impulsar", "status.cannot_reblog": "Esta publicación no se puede impulsar",
"status.context.load_new_replies": "Hay nuevas respuestas",
"status.context.loading": "Buscando más respuestas",
"status.continued_thread": "Continuó el hilo", "status.continued_thread": "Continuó el hilo",
"status.copy": "Copiar enlace a la publicación", "status.copy": "Copiar enlace a la publicación",
"status.delete": "Borrar", "status.delete": "Borrar",
@ -873,12 +880,11 @@
"status.open": "Expandir publicación", "status.open": "Expandir publicación",
"status.pin": "Fijar", "status.pin": "Fijar",
"status.quote_error.filtered": "Oculto debido a uno de tus filtros", "status.quote_error.filtered": "Oculto debido a uno de tus filtros",
"status.quote_error.not_found": "No se puede mostrar esta publicación.", "status.quote_error.not_available": "Publicación no disponible",
"status.quote_error.pending_approval": "Esta publicación está pendiente de aprobación del autor original.", "status.quote_error.pending_approval": "Publicación pendiente",
"status.quote_error.rejected": "Esta publicación no puede mostrarse porque el autor original no permite que se cite.", "status.quote_error.pending_approval_popout.body": "Las citas compartidas a través del Fediverso pueden tardar en mostrarse, ya que los diferentes servidores tienen diferentes protocolos.",
"status.quote_error.removed": "Esta publicación fue eliminada por su autor.", "status.quote_error.pending_approval_popout.title": "¿Cita pendiente? Mantén la calma",
"status.quote_error.unauthorized": "Esta publicación no puede mostrarse, ya que no estás autorizado a verla.", "status.quote_post_author": "Ha citado una publicación de @{name}",
"status.quote_post_author": "Publicación de {name}",
"status.read_more": "Leer más", "status.read_more": "Leer más",
"status.reblog": "Impulsar", "status.reblog": "Impulsar",
"status.reblog_private": "Impulsar a la audiencia original", "status.reblog_private": "Impulsar a la audiencia original",
@ -893,6 +899,7 @@
"status.reply": "Responder", "status.reply": "Responder",
"status.replyAll": "Responder al hilo", "status.replyAll": "Responder al hilo",
"status.report": "Reportar a @{name}", "status.report": "Reportar a @{name}",
"status.revoke_quote": "Eliminar mi publicación de la cita de {name}",
"status.sensitive_warning": "Contenido sensible", "status.sensitive_warning": "Contenido sensible",
"status.share": "Compartir", "status.share": "Compartir",
"status.show_less_all": "Mostrar menos para todo", "status.show_less_all": "Mostrar menos para todo",

View File

@ -424,8 +424,6 @@
"hints.profiles.see_more_followers": "Vaata rohkem jälgijaid kohas {domain}", "hints.profiles.see_more_followers": "Vaata rohkem jälgijaid kohas {domain}",
"hints.profiles.see_more_follows": "Vaata rohkem jälgitavaid kohas {domain}", "hints.profiles.see_more_follows": "Vaata rohkem jälgitavaid kohas {domain}",
"hints.profiles.see_more_posts": "Vaata rohkem postitusi kohas {domain}", "hints.profiles.see_more_posts": "Vaata rohkem postitusi kohas {domain}",
"hints.threads.replies_may_be_missing": "Vastuseid teistest serveritest võib olla puudu.",
"hints.threads.see_more": "Vaata rohkem vastuseid kohas {domain}",
"home.column_settings.show_quotes": "Näita tsiteeritut", "home.column_settings.show_quotes": "Näita tsiteeritut",
"home.column_settings.show_reblogs": "Näita jagamisi", "home.column_settings.show_reblogs": "Näita jagamisi",
"home.column_settings.show_replies": "Näita vastuseid", "home.column_settings.show_replies": "Näita vastuseid",
@ -500,6 +498,8 @@
"keyboard_shortcuts.translate": "postituse tõlkimiseks", "keyboard_shortcuts.translate": "postituse tõlkimiseks",
"keyboard_shortcuts.unfocus": "Fookus tekstialalt/otsingult ära", "keyboard_shortcuts.unfocus": "Fookus tekstialalt/otsingult ära",
"keyboard_shortcuts.up": "Liigu loetelus üles", "keyboard_shortcuts.up": "Liigu loetelus üles",
"learn_more_link.got_it": "Sain aru",
"learn_more_link.learn_more": "Lisateave",
"lightbox.close": "Sulge", "lightbox.close": "Sulge",
"lightbox.next": "Järgmine", "lightbox.next": "Järgmine",
"lightbox.previous": "Eelmine", "lightbox.previous": "Eelmine",
@ -847,6 +847,8 @@
"status.bookmark": "Järjehoidja", "status.bookmark": "Järjehoidja",
"status.cancel_reblog_private": "Lõpeta jagamine", "status.cancel_reblog_private": "Lõpeta jagamine",
"status.cannot_reblog": "Seda postitust ei saa jagada", "status.cannot_reblog": "Seda postitust ei saa jagada",
"status.context.load_new_replies": "Leidub uusi vastuseid",
"status.context.loading": "Kontrollin täiendavate vastuste olemasolu",
"status.continued_thread": "Jätkatud lõim", "status.continued_thread": "Jätkatud lõim",
"status.copy": "Kopeeri postituse link", "status.copy": "Kopeeri postituse link",
"status.delete": "Kustuta", "status.delete": "Kustuta",
@ -873,12 +875,11 @@
"status.open": "Laienda postitus", "status.open": "Laienda postitus",
"status.pin": "Kinnita profiilile", "status.pin": "Kinnita profiilile",
"status.quote_error.filtered": "Peidetud mõne kasutatud filtri tõttu", "status.quote_error.filtered": "Peidetud mõne kasutatud filtri tõttu",
"status.quote_error.not_found": "Seda postitust ei saa näidata.", "status.quote_error.not_available": "Postitus pole saadaval",
"status.quote_error.pending_approval": "See postitus on algse autori kinnituse ootel.", "status.quote_error.pending_approval": "Postitus on ootel",
"status.quote_error.rejected": "Seda postitust ei saa näidata, kuina algne autor ei luba teda tsiteerida.", "status.quote_error.pending_approval_popout.body": "Kuna erinevates serverites on erinevad reeglid, siis üle Födiversumi jagatud tsitaatide kuvamine võib võtta aega.",
"status.quote_error.removed": "Autor kustutas selle postituse.", "status.quote_error.pending_approval_popout.title": "Tsiteerimine on ootel? Palun jää rahulikuks",
"status.quote_error.unauthorized": "Kuna sul pole luba selle postituse nägemiseks, siis seda ei saa kuvada.", "status.quote_post_author": "Tsiteeris kasutaja @{name} postitust",
"status.quote_post_author": "Postitajaks {name}",
"status.read_more": "Loe veel", "status.read_more": "Loe veel",
"status.reblog": "Jaga", "status.reblog": "Jaga",
"status.reblog_private": "Jaga algse nähtavusega", "status.reblog_private": "Jaga algse nähtavusega",

View File

@ -30,6 +30,7 @@
"account.edit_profile": "Editatu profila", "account.edit_profile": "Editatu profila",
"account.enable_notifications": "Jakinarazi @{name} erabiltzaileak argitaratzean", "account.enable_notifications": "Jakinarazi @{name} erabiltzaileak argitaratzean",
"account.endorse": "Nabarmendu profilean", "account.endorse": "Nabarmendu profilean",
"account.familiar_followers_many": "Jarraitzaileak: {name1}, {name2} eta beste {othersCount, plural, one {ezagun bat} other {# ezagun}}",
"account.familiar_followers_one": "{name1}-k jarraitzen du", "account.familiar_followers_one": "{name1}-k jarraitzen du",
"account.familiar_followers_two": "{name1}-k eta {name2}-k jarraitzen dute", "account.familiar_followers_two": "{name1}-k eta {name2}-k jarraitzen dute",
"account.featured": "Gailenak", "account.featured": "Gailenak",
@ -118,6 +119,8 @@
"annual_report.summary.most_used_hashtag.most_used_hashtag": "traola erabiliena", "annual_report.summary.most_used_hashtag.most_used_hashtag": "traola erabiliena",
"annual_report.summary.most_used_hashtag.none": "Bat ere ez", "annual_report.summary.most_used_hashtag.none": "Bat ere ez",
"annual_report.summary.new_posts.new_posts": "bidalketa berriak", "annual_report.summary.new_posts.new_posts": "bidalketa berriak",
"annual_report.summary.percentile.text": "<topLabel>Horrek jartzen zaitu top </topLabel> <percentage> </percentage>(e)an <bottomLabel> {domain} erabiltzaileen artean </bottomLabel>",
"annual_report.summary.percentile.we_wont_tell_bernie": "Bernieri ez diogu ezer esango ;)..",
"annual_report.summary.thanks": "Eskerrik asko Mastodonen parte izateagatik!", "annual_report.summary.thanks": "Eskerrik asko Mastodonen parte izateagatik!",
"attachments_list.unprocessed": "(prozesatu gabe)", "attachments_list.unprocessed": "(prozesatu gabe)",
"audio.hide": "Ezkutatu audioa", "audio.hide": "Ezkutatu audioa",
@ -216,6 +219,7 @@
"confirmations.discard_draft.edit.message": "Jarraitzeak editatzen ari zaren mezuan egindako aldaketak baztertuko ditu.", "confirmations.discard_draft.edit.message": "Jarraitzeak editatzen ari zaren mezuan egindako aldaketak baztertuko ditu.",
"confirmations.discard_draft.edit.title": "Baztertu zure argitalpenari egindako aldaketak?", "confirmations.discard_draft.edit.title": "Baztertu zure argitalpenari egindako aldaketak?",
"confirmations.discard_draft.post.cancel": "Zirriborroa berrekin", "confirmations.discard_draft.post.cancel": "Zirriborroa berrekin",
"confirmations.discard_draft.post.message": "Jarraituz gero, idazten ari zaren sarrera bertan behera geratuko da.",
"confirmations.discard_draft.post.title": "Zure argitalpenaren zirriborroa baztertu nahi duzu?", "confirmations.discard_draft.post.title": "Zure argitalpenaren zirriborroa baztertu nahi duzu?",
"confirmations.discard_edit_media.confirm": "Baztertu", "confirmations.discard_edit_media.confirm": "Baztertu",
"confirmations.discard_edit_media.message": "Multimediaren deskribapen edo aurrebistan gorde gabeko aldaketak daude, baztertu nahi dituzu?", "confirmations.discard_edit_media.message": "Multimediaren deskribapen edo aurrebistan gorde gabeko aldaketak daude, baztertu nahi dituzu?",
@ -413,8 +417,6 @@
"hints.profiles.see_more_followers": "Ikusi jarraitzaile gehiago {domain}-(e)n", "hints.profiles.see_more_followers": "Ikusi jarraitzaile gehiago {domain}-(e)n",
"hints.profiles.see_more_follows": "Ikusi jarraitzaile gehiago {domain}-(e)n", "hints.profiles.see_more_follows": "Ikusi jarraitzaile gehiago {domain}-(e)n",
"hints.profiles.see_more_posts": "Ikusi bidalketa gehiago {domain}-(e)n", "hints.profiles.see_more_posts": "Ikusi bidalketa gehiago {domain}-(e)n",
"hints.threads.replies_may_be_missing": "Baliteke beste zerbitzari batzuen erantzun batzuk ez erakustea.",
"hints.threads.see_more": "Ikusi erantzun gehiago {domain}-(e)n",
"home.column_settings.show_quotes": "Erakutsi aipamenak", "home.column_settings.show_quotes": "Erakutsi aipamenak",
"home.column_settings.show_reblogs": "Erakutsi bultzadak", "home.column_settings.show_reblogs": "Erakutsi bultzadak",
"home.column_settings.show_replies": "Erakutsi erantzunak", "home.column_settings.show_replies": "Erakutsi erantzunak",
@ -435,6 +437,7 @@
"ignore_notifications_modal.not_following_title": "Jarraitzen ez dituzun pertsonen jakinarazpenei ez ikusiarena egin?", "ignore_notifications_modal.not_following_title": "Jarraitzen ez dituzun pertsonen jakinarazpenei ez ikusiarena egin?",
"ignore_notifications_modal.private_mentions_title": "Eskatu gabeko aipamen pribatuen jakinarazpenei ez ikusiarena egin?", "ignore_notifications_modal.private_mentions_title": "Eskatu gabeko aipamen pribatuen jakinarazpenei ez ikusiarena egin?",
"info_button.label": "Laguntza", "info_button.label": "Laguntza",
"info_button.what_is_alt_text": "<h1>Zer da Alt testua?</h1><p>Alt testuak irudiak deskribatzeko aukera ematen du, ikusmen-urritasunak, banda-zabalera txikiko konexioak edo testuinguru gehigarria nahi duten pertsonentzat.</p><p>Alt testu argi, zehatz eta objektiboen bidez, guztion irisgarritasuna eta ulermena hobetu ditzakezu.</p><ul><li>Hartu elementu garrantzitsuenak</li><li>Laburbildu irudietako testua</li><li>Erabili esaldien egitura erregularra</li><li>Baztertu informazio erredundantea.</li><li>Enfokatu joeretan eta funtsezko elementuetan irudi konplexuetan (diagrametan edo mapetan, adibidez)</li></ul>",
"interaction_modal.action.favourite": "Jarraitzeko, zure kontutik atsegindu behar duzu.", "interaction_modal.action.favourite": "Jarraitzeko, zure kontutik atsegindu behar duzu.",
"interaction_modal.action.follow": "Jarraitzeko zure kontutik jarraitu behar duzu.", "interaction_modal.action.follow": "Jarraitzeko zure kontutik jarraitu behar duzu.",
"interaction_modal.action.reply": "Jarraitzeko zure kontutik erantzun behar duzu.", "interaction_modal.action.reply": "Jarraitzeko zure kontutik erantzun behar duzu.",
@ -841,8 +844,6 @@
"status.mute_conversation": "Mututu elkarrizketa", "status.mute_conversation": "Mututu elkarrizketa",
"status.open": "Hedatu bidalketa hau", "status.open": "Hedatu bidalketa hau",
"status.pin": "Finkatu profilean", "status.pin": "Finkatu profilean",
"status.quote_error.not_found": "Bidalketa hau ezin da erakutsi.",
"status.quote_error.pending_approval": "Bidalketa hau egile originalak onartzeko zain dago.",
"status.read_more": "Irakurri gehiago", "status.read_more": "Irakurri gehiago",
"status.reblog": "Bultzada", "status.reblog": "Bultzada",
"status.reblog_private": "Bultzada jatorrizko hartzaileei", "status.reblog_private": "Bultzada jatorrizko hartzaileei",
@ -903,6 +904,7 @@
"video.hide": "Ezkutatu bideoa", "video.hide": "Ezkutatu bideoa",
"video.pause": "Pausatu", "video.pause": "Pausatu",
"video.play": "Jo", "video.play": "Jo",
"video.skip_forward": "Jauzi aurrerantz",
"video.unmute": "Soinua ezarri", "video.unmute": "Soinua ezarri",
"video.volume_down": "Bolumena jaitsi", "video.volume_down": "Bolumena jaitsi",
"video.volume_up": "Bolumena Igo" "video.volume_up": "Bolumena Igo"

View File

@ -235,7 +235,7 @@
"confirmations.logout.message": "مطمئنید می‌خواهید خارج شوید؟", "confirmations.logout.message": "مطمئنید می‌خواهید خارج شوید؟",
"confirmations.logout.title": "خروج؟", "confirmations.logout.title": "خروج؟",
"confirmations.missing_alt_text.confirm": "متن جایگزین را اضافه کنید", "confirmations.missing_alt_text.confirm": "متن جایگزین را اضافه کنید",
"confirmations.missing_alt_text.message": "پست شما حاوی رسانه بدون متن جایگزین است. افزودن توضیحات کمک می کند تا محتوای شما برای افراد بیشتری قابل دسترسی باشد.", "confirmations.missing_alt_text.message": "فرسته‌تان رسانه‌هایی بدون متن جایگزین دارد. افزودن شرح به دسترس‌پذیر شدن محتوایتان برای افراد بیش‌تری کمک می‌کند.",
"confirmations.missing_alt_text.secondary": "به هر حال پست کن", "confirmations.missing_alt_text.secondary": "به هر حال پست کن",
"confirmations.missing_alt_text.title": "متن جایگزین اضافه شود؟", "confirmations.missing_alt_text.title": "متن جایگزین اضافه شود؟",
"confirmations.mute.confirm": "خموش", "confirmations.mute.confirm": "خموش",
@ -245,6 +245,9 @@
"confirmations.remove_from_followers.confirm": "برداشتن پی‌گیرنده", "confirmations.remove_from_followers.confirm": "برداشتن پی‌گیرنده",
"confirmations.remove_from_followers.message": "دیگر {name} پیتان نخواهد گرفت. مطمئنید که می‌خواهید ادامه دهید؟", "confirmations.remove_from_followers.message": "دیگر {name} پیتان نخواهد گرفت. مطمئنید که می‌خواهید ادامه دهید؟",
"confirmations.remove_from_followers.title": "برداشتن پی‌گیرنده؟", "confirmations.remove_from_followers.title": "برداشتن پی‌گیرنده؟",
"confirmations.revoke_quote.confirm": "حذف فرسته",
"confirmations.revoke_quote.message": "این اقدام قابل بازگشت نیست.",
"confirmations.revoke_quote.title": "آیا فرسته را حذف کنم؟",
"confirmations.unfollow.confirm": "پی‌نگرفتن", "confirmations.unfollow.confirm": "پی‌نگرفتن",
"confirmations.unfollow.message": "مطمئنید که می‌خواهید به پی‌گیری از {name} پایان دهید؟", "confirmations.unfollow.message": "مطمئنید که می‌خواهید به پی‌گیری از {name} پایان دهید؟",
"confirmations.unfollow.title": "ناپی‌گیری کاربر؟", "confirmations.unfollow.title": "ناپی‌گیری کاربر؟",
@ -424,9 +427,7 @@
"hints.profiles.see_more_followers": "دیدن پی‌گیرندگان بیش‌تر روی {domain}", "hints.profiles.see_more_followers": "دیدن پی‌گیرندگان بیش‌تر روی {domain}",
"hints.profiles.see_more_follows": "دیدن پی‌گرفته‌های بیش‌تر روی {domain}", "hints.profiles.see_more_follows": "دیدن پی‌گرفته‌های بیش‌تر روی {domain}",
"hints.profiles.see_more_posts": "دیدن فرسته‌های بیش‌تر روی {domain}", "hints.profiles.see_more_posts": "دیدن فرسته‌های بیش‌تر روی {domain}",
"hints.threads.replies_may_be_missing": "شاید پاسخ‌ها از دیگر کارسازها نباشند.", "home.column_settings.show_quotes": "نمایش نقل‌ها",
"hints.threads.see_more": "دیدن پاسخ‌های بیش‌تر روی {domain}",
"home.column_settings.show_quotes": "نمایش نقل‌قول‌ها",
"home.column_settings.show_reblogs": "نمایش تقویت‌ها", "home.column_settings.show_reblogs": "نمایش تقویت‌ها",
"home.column_settings.show_replies": "نمایش پاسخ‌ها", "home.column_settings.show_replies": "نمایش پاسخ‌ها",
"home.hide_announcements": "نهفتن اعلامیه‌ها", "home.hide_announcements": "نهفتن اعلامیه‌ها",
@ -500,6 +501,8 @@
"keyboard_shortcuts.translate": "برای ترجمه یک پست", "keyboard_shortcuts.translate": "برای ترجمه یک پست",
"keyboard_shortcuts.unfocus": "برداشتن تمرکز از ناحیهٔ نوشتن یا جست‌وجو", "keyboard_shortcuts.unfocus": "برداشتن تمرکز از ناحیهٔ نوشتن یا جست‌وجو",
"keyboard_shortcuts.up": "بالا بردن در سیاهه", "keyboard_shortcuts.up": "بالا بردن در سیاهه",
"learn_more_link.got_it": "متوجه شدم",
"learn_more_link.learn_more": "دانستن بیش‌تر",
"lightbox.close": "بستن", "lightbox.close": "بستن",
"lightbox.next": "بعدی", "lightbox.next": "بعدی",
"lightbox.previous": "قبلی", "lightbox.previous": "قبلی",
@ -600,6 +603,7 @@
"notification.label.mention": "اشاره", "notification.label.mention": "اشاره",
"notification.label.private_mention": "اشارهٔ خصوصی", "notification.label.private_mention": "اشارهٔ خصوصی",
"notification.label.private_reply": "پاسخ خصوصی", "notification.label.private_reply": "پاسخ خصوصی",
"notification.label.quote": "{name} فرسته‌تان را نقل کرد",
"notification.label.reply": "پاسخ", "notification.label.reply": "پاسخ",
"notification.mention": "اشاره", "notification.mention": "اشاره",
"notification.mentioned_you": "{name} از شما نام برد", "notification.mentioned_you": "{name} از شما نام برد",
@ -657,6 +661,7 @@
"notifications.column_settings.mention": "اشاره‌ها:", "notifications.column_settings.mention": "اشاره‌ها:",
"notifications.column_settings.poll": "نتایج نظرسنجی:", "notifications.column_settings.poll": "نتایج نظرسنجی:",
"notifications.column_settings.push": "آگاهی‌های ارسالی", "notifications.column_settings.push": "آگاهی‌های ارسالی",
"notifications.column_settings.quote": "نقل‌قول‌ها:",
"notifications.column_settings.reblog": "تقویت‌ها:", "notifications.column_settings.reblog": "تقویت‌ها:",
"notifications.column_settings.show": "نمایش در ستون", "notifications.column_settings.show": "نمایش در ستون",
"notifications.column_settings.sound": "پخش صدا", "notifications.column_settings.sound": "پخش صدا",
@ -847,6 +852,8 @@
"status.bookmark": "نشانک", "status.bookmark": "نشانک",
"status.cancel_reblog_private": "ناتقویت", "status.cancel_reblog_private": "ناتقویت",
"status.cannot_reblog": "این فرسته قابل تقویت نیست", "status.cannot_reblog": "این فرسته قابل تقویت نیست",
"status.context.load_new_replies": "پاسخ‌های جدیدی موجودند",
"status.context.loading": "بررسی کردن برای پاسخ‌های بیش‌تر",
"status.continued_thread": "رشتهٔ دنباله دار", "status.continued_thread": "رشتهٔ دنباله دار",
"status.copy": "رونوشت از پیوند فرسته", "status.copy": "رونوشت از پیوند فرسته",
"status.delete": "حذف", "status.delete": "حذف",
@ -873,12 +880,9 @@
"status.open": "گسترش این فرسته", "status.open": "گسترش این فرسته",
"status.pin": "سنجاق به نمایه", "status.pin": "سنجاق به نمایه",
"status.quote_error.filtered": "نهفته بنا بر یکی از پالایه‌هایتان", "status.quote_error.filtered": "نهفته بنا بر یکی از پالایه‌هایتان",
"status.quote_error.not_found": "این فرسته قابل نمایش نیست.", "status.quote_error.not_available": "فرسته در دسترس نیست",
"status.quote_error.pending_approval": "این فرسته منظر تأیید نگارندهٔ اصلی است.", "status.quote_error.pending_approval_popout.body": "نقل‌قول‌هایی که در سراسر فدیورس هم‌رسانی می‌شوند ممکن است زمان‌بر باشند تا نمایش داده شوند، چون کارسازهای مختلف از شیوه‌نامه‌های متفاوتی استفاده می‌کنند.",
"status.quote_error.rejected": "از آن‌جا که نگارندهٔ اصلی فرسته اجازهٔ نقل قولش را نمی‌دهد این فرسته قابل نمایش نیست.", "status.quote_post_author": "فرسته‌ای از @{name} نقل شد",
"status.quote_error.removed": "این فرسته به دست نگارنده‌اش برداشته شده.",
"status.quote_error.unauthorized": "از آن‌جا که اجازهٔ دیدن این فرسته را ندارید قابل نمایش نیست.",
"status.quote_post_author": "فرسته توسط {name}",
"status.read_more": "بیشتر بخوانید", "status.read_more": "بیشتر بخوانید",
"status.reblog": "تقویت", "status.reblog": "تقویت",
"status.reblog_private": "تقویت برای مخاطبان نخستین", "status.reblog_private": "تقویت برای مخاطبان نخستین",
@ -893,6 +897,7 @@
"status.reply": "پاسخ", "status.reply": "پاسخ",
"status.replyAll": "پاسخ به رشته", "status.replyAll": "پاسخ به رشته",
"status.report": "گزارش @{name}", "status.report": "گزارش @{name}",
"status.revoke_quote": "حذف فرسته‌ام از فرسته @{name}",
"status.sensitive_warning": "محتوای حساس", "status.sensitive_warning": "محتوای حساس",
"status.share": "هم‌رسانی", "status.share": "هم‌رسانی",
"status.show_less_all": "نمایش کمتر همه", "status.show_less_all": "نمایش کمتر همه",

View File

@ -311,7 +311,7 @@
"empty_column.account_featured_other.unknown": "Tämä tili ei suosittele vielä mitään.", "empty_column.account_featured_other.unknown": "Tämä tili ei suosittele vielä mitään.",
"empty_column.account_hides_collections": "Käyttäjä on päättänyt pitää nämä tiedot yksityisinä", "empty_column.account_hides_collections": "Käyttäjä on päättänyt pitää nämä tiedot yksityisinä",
"empty_column.account_suspended": "Tili jäädytetty", "empty_column.account_suspended": "Tili jäädytetty",
"empty_column.account_timeline": "Ei viestejä täällä.", "empty_column.account_timeline": "Ei julkaisuja täällä!",
"empty_column.account_unavailable": "Profiilia ei ole saatavilla", "empty_column.account_unavailable": "Profiilia ei ole saatavilla",
"empty_column.blocks": "Et ole vielä estänyt käyttäjiä.", "empty_column.blocks": "Et ole vielä estänyt käyttäjiä.",
"empty_column.bookmarked_statuses": "Et ole vielä lisännyt julkaisuja kirjanmerkkeihisi. Kun lisäät yhden, se näkyy tässä.", "empty_column.bookmarked_statuses": "Et ole vielä lisännyt julkaisuja kirjanmerkkeihisi. Kun lisäät yhden, se näkyy tässä.",
@ -424,8 +424,6 @@
"hints.profiles.see_more_followers": "Näytä lisää seuraajia palvelimella {domain}", "hints.profiles.see_more_followers": "Näytä lisää seuraajia palvelimella {domain}",
"hints.profiles.see_more_follows": "Näytä lisää seurattavia palvelimella {domain}", "hints.profiles.see_more_follows": "Näytä lisää seurattavia palvelimella {domain}",
"hints.profiles.see_more_posts": "Näytä lisää julkaisuja palvelimella {domain}", "hints.profiles.see_more_posts": "Näytä lisää julkaisuja palvelimella {domain}",
"hints.threads.replies_may_be_missing": "Muiden palvelinten vastauksia saattaa puuttua.",
"hints.threads.see_more": "Näytä lisää vastauksia palvelimella {domain}",
"home.column_settings.show_quotes": "Näytä lainaukset", "home.column_settings.show_quotes": "Näytä lainaukset",
"home.column_settings.show_reblogs": "Näytä tehostukset", "home.column_settings.show_reblogs": "Näytä tehostukset",
"home.column_settings.show_replies": "Näytä vastaukset", "home.column_settings.show_replies": "Näytä vastaukset",
@ -500,6 +498,8 @@
"keyboard_shortcuts.translate": "Käännä julkaisu", "keyboard_shortcuts.translate": "Käännä julkaisu",
"keyboard_shortcuts.unfocus": "Poistu kirjoitus- tai hakukentästä", "keyboard_shortcuts.unfocus": "Poistu kirjoitus- tai hakukentästä",
"keyboard_shortcuts.up": "Siirry luettelossa taaksepäin", "keyboard_shortcuts.up": "Siirry luettelossa taaksepäin",
"learn_more_link.got_it": "Selvä",
"learn_more_link.learn_more": "Lue lisää",
"lightbox.close": "Sulje", "lightbox.close": "Sulje",
"lightbox.next": "Seuraava", "lightbox.next": "Seuraava",
"lightbox.previous": "Edellinen", "lightbox.previous": "Edellinen",
@ -600,6 +600,7 @@
"notification.label.mention": "Maininta", "notification.label.mention": "Maininta",
"notification.label.private_mention": "Yksityismaininta", "notification.label.private_mention": "Yksityismaininta",
"notification.label.private_reply": "Yksityinen vastaus", "notification.label.private_reply": "Yksityinen vastaus",
"notification.label.quote": "{name} lainasi julkaisuasi",
"notification.label.reply": "Vastaus", "notification.label.reply": "Vastaus",
"notification.mention": "Maininta", "notification.mention": "Maininta",
"notification.mentioned_you": "{name} mainitsi sinut", "notification.mentioned_you": "{name} mainitsi sinut",
@ -657,6 +658,7 @@
"notifications.column_settings.mention": "Maininnat:", "notifications.column_settings.mention": "Maininnat:",
"notifications.column_settings.poll": "Äänestystulokset:", "notifications.column_settings.poll": "Äänestystulokset:",
"notifications.column_settings.push": "Puskuilmoitukset", "notifications.column_settings.push": "Puskuilmoitukset",
"notifications.column_settings.quote": "Lainaukset:",
"notifications.column_settings.reblog": "Tehostukset:", "notifications.column_settings.reblog": "Tehostukset:",
"notifications.column_settings.show": "Näytä sarakkeessa", "notifications.column_settings.show": "Näytä sarakkeessa",
"notifications.column_settings.sound": "Äänimerkki", "notifications.column_settings.sound": "Äänimerkki",
@ -756,7 +758,7 @@
"reply_indicator.cancel": "Peruuta", "reply_indicator.cancel": "Peruuta",
"reply_indicator.poll": "Äänestys", "reply_indicator.poll": "Äänestys",
"report.block": "Estä", "report.block": "Estä",
"report.block_explanation": "Et näe hänen viestejään, eikä hän voi nähdä viestejäsi tai seurata sinua. Hän näkee, että olet estänyt hänet.", "report.block_explanation": "Et näe hänen julkaisujaan. Hän ei voi nähdä julkaisujasi eikä seurata sinua. Hän näkee, että olet estänyt hänet.",
"report.categories.legal": "Lakiseikat", "report.categories.legal": "Lakiseikat",
"report.categories.other": "Muu", "report.categories.other": "Muu",
"report.categories.spam": "Roskaposti", "report.categories.spam": "Roskaposti",
@ -847,6 +849,8 @@
"status.bookmark": "Lisää kirjanmerkki", "status.bookmark": "Lisää kirjanmerkki",
"status.cancel_reblog_private": "Peru tehostus", "status.cancel_reblog_private": "Peru tehostus",
"status.cannot_reblog": "Tätä julkaisua ei voi tehostaa", "status.cannot_reblog": "Tätä julkaisua ei voi tehostaa",
"status.context.load_new_replies": "Uusia vastauksia saatavilla",
"status.context.loading": "Tarkistetaan lisävastauksia",
"status.continued_thread": "Jatkoi ketjua", "status.continued_thread": "Jatkoi ketjua",
"status.copy": "Kopioi linkki julkaisuun", "status.copy": "Kopioi linkki julkaisuun",
"status.delete": "Poista", "status.delete": "Poista",
@ -873,12 +877,11 @@
"status.open": "Laajenna julkaisu", "status.open": "Laajenna julkaisu",
"status.pin": "Kiinnitä profiiliin", "status.pin": "Kiinnitä profiiliin",
"status.quote_error.filtered": "Piilotettu jonkin asettamasi suodattimen takia", "status.quote_error.filtered": "Piilotettu jonkin asettamasi suodattimen takia",
"status.quote_error.not_found": "Tätä julkaisua ei voi näyttää.", "status.quote_error.not_available": "Julkaisu ei saatavilla",
"status.quote_error.pending_approval": "Tämä julkaisu odottaa alkuperäisen tekijänsä hyväksyntää.", "status.quote_error.pending_approval": "Julkaisu odottaa",
"status.quote_error.rejected": "Tätä julkaisua ei voi näyttää, sillä sen alkuperäinen tekijä ei salli lainattavan julkaisua.", "status.quote_error.pending_approval_popout.body": "Saattaa viedä jonkin ainaa ennen kuin fediversumin kautta jaetut julkaisut tulevat näkyviin, sillä eri palvelimet käyttävät eri protokollia.",
"status.quote_error.removed": "Tekijä on poistanut julkaisun.", "status.quote_error.pending_approval_popout.title": "Odottava lainaus? Pysy rauhallisena",
"status.quote_error.unauthorized": "Tätä julkaisua ei voi näyttää, koska sinulla ei ole oikeutta tarkastella sitä.", "status.quote_post_author": "Lainaa käyttäjän @{name} julkaisua",
"status.quote_post_author": "Julkaisu käyttäjältä {name}",
"status.read_more": "Näytä enemmän", "status.read_more": "Näytä enemmän",
"status.reblog": "Tehosta", "status.reblog": "Tehosta",
"status.reblog_private": "Tehosta alkuperäiselle yleisölle", "status.reblog_private": "Tehosta alkuperäiselle yleisölle",

View File

@ -245,6 +245,9 @@
"confirmations.remove_from_followers.confirm": "Strika fylgjara", "confirmations.remove_from_followers.confirm": "Strika fylgjara",
"confirmations.remove_from_followers.message": "{name} fer ikki longur at fylgja tær. Er tú vís/ur í at tú vilt halda fram?", "confirmations.remove_from_followers.message": "{name} fer ikki longur at fylgja tær. Er tú vís/ur í at tú vilt halda fram?",
"confirmations.remove_from_followers.title": "Strika fylgjara?", "confirmations.remove_from_followers.title": "Strika fylgjara?",
"confirmations.revoke_quote.confirm": "Strika post",
"confirmations.revoke_quote.message": "Hendan atgerðin kann ikki angrast.",
"confirmations.revoke_quote.title": "Strika post?",
"confirmations.unfollow.confirm": "Fylg ikki", "confirmations.unfollow.confirm": "Fylg ikki",
"confirmations.unfollow.message": "Ert tú vís/ur í, at tú vil steðga við at fylgja {name}?", "confirmations.unfollow.message": "Ert tú vís/ur í, at tú vil steðga við at fylgja {name}?",
"confirmations.unfollow.title": "Gevst at fylgja brúkara?", "confirmations.unfollow.title": "Gevst at fylgja brúkara?",
@ -424,8 +427,6 @@
"hints.profiles.see_more_followers": "Sí fleiri fylgjarar á {domain}", "hints.profiles.see_more_followers": "Sí fleiri fylgjarar á {domain}",
"hints.profiles.see_more_follows": "Sí fleiri, ið viðkomandi fylgir, á {domain}", "hints.profiles.see_more_follows": "Sí fleiri, ið viðkomandi fylgir, á {domain}",
"hints.profiles.see_more_posts": "Sí fleiri postar á {domain}", "hints.profiles.see_more_posts": "Sí fleiri postar á {domain}",
"hints.threads.replies_may_be_missing": "Svar frá øðrum ambætarum mangla møguliga.",
"hints.threads.see_more": "Sí fleiri svar á {domain}",
"home.column_settings.show_quotes": "Vís siteringar", "home.column_settings.show_quotes": "Vís siteringar",
"home.column_settings.show_reblogs": "Vís lyft", "home.column_settings.show_reblogs": "Vís lyft",
"home.column_settings.show_replies": "Vís svar", "home.column_settings.show_replies": "Vís svar",
@ -500,6 +501,8 @@
"keyboard_shortcuts.translate": "at umseta ein post", "keyboard_shortcuts.translate": "at umseta ein post",
"keyboard_shortcuts.unfocus": "Tak skrivi-/leiti-økið úr miðdeplinum", "keyboard_shortcuts.unfocus": "Tak skrivi-/leiti-økið úr miðdeplinum",
"keyboard_shortcuts.up": "Flyt upp á listanum", "keyboard_shortcuts.up": "Flyt upp á listanum",
"learn_more_link.got_it": "Eg skilji",
"learn_more_link.learn_more": "Lær meira",
"lightbox.close": "Lat aftur", "lightbox.close": "Lat aftur",
"lightbox.next": "Fram", "lightbox.next": "Fram",
"lightbox.previous": "Aftur", "lightbox.previous": "Aftur",
@ -600,6 +603,7 @@
"notification.label.mention": "Umrøða", "notification.label.mention": "Umrøða",
"notification.label.private_mention": "Privat umrøða", "notification.label.private_mention": "Privat umrøða",
"notification.label.private_reply": "Privat svar", "notification.label.private_reply": "Privat svar",
"notification.label.quote": "{name} siteraði postin hjá tær",
"notification.label.reply": "Svara", "notification.label.reply": "Svara",
"notification.mention": "Umrøð", "notification.mention": "Umrøð",
"notification.mentioned_you": "{name} nevndi teg", "notification.mentioned_you": "{name} nevndi teg",
@ -657,6 +661,7 @@
"notifications.column_settings.mention": "Umrøður:", "notifications.column_settings.mention": "Umrøður:",
"notifications.column_settings.poll": "Úrslit frá atkvøðugreiðslu:", "notifications.column_settings.poll": "Úrslit frá atkvøðugreiðslu:",
"notifications.column_settings.push": "Trýstifráboðanir", "notifications.column_settings.push": "Trýstifráboðanir",
"notifications.column_settings.quote": "Sitatir:",
"notifications.column_settings.reblog": "Stimbranir:", "notifications.column_settings.reblog": "Stimbranir:",
"notifications.column_settings.show": "Vís í teigi", "notifications.column_settings.show": "Vís í teigi",
"notifications.column_settings.sound": "Spæl ljóð", "notifications.column_settings.sound": "Spæl ljóð",
@ -847,6 +852,8 @@
"status.bookmark": "Goym", "status.bookmark": "Goym",
"status.cancel_reblog_private": "Strika stimbran", "status.cancel_reblog_private": "Strika stimbran",
"status.cannot_reblog": "Tað ber ikki til at stimbra hendan postin", "status.cannot_reblog": "Tað ber ikki til at stimbra hendan postin",
"status.context.load_new_replies": "Nýggj svar tøk",
"status.context.loading": "Kanni um tað eru fleiri svar",
"status.continued_thread": "Framhaldandi tráður", "status.continued_thread": "Framhaldandi tráður",
"status.copy": "Kopiera leinki til postin", "status.copy": "Kopiera leinki til postin",
"status.delete": "Strika", "status.delete": "Strika",
@ -873,12 +880,11 @@
"status.open": "Víðka henda postin", "status.open": "Víðka henda postin",
"status.pin": "Ger fastan í vangan", "status.pin": "Ger fastan í vangan",
"status.quote_error.filtered": "Eitt av tínum filtrum fjalir hetta", "status.quote_error.filtered": "Eitt av tínum filtrum fjalir hetta",
"status.quote_error.not_found": "Tað ber ikki til at vísa hendan postin.", "status.quote_error.not_available": "Postur ikki tøkur",
"status.quote_error.pending_approval": "Hesin posturin bíðar eftir góðkenning frá upprunahøvundinum.", "status.quote_error.pending_approval": "Postur bíðar",
"status.quote_error.rejected": "Hesin posturin kann ikki vísast, tí upprunahøvundurin loyvir ikki at posturin verður siteraður.", "status.quote_error.pending_approval_popout.body": "Sitatir, sum eru deild tvørtur um fediversið, kunnu taka nakað av tíð at vísast, tí ymiskir ambætarar hava ymiskar protokollir.",
"status.quote_error.removed": "Hesin posturin var strikaður av høvundinum.", "status.quote_error.pending_approval_popout.title": "Bíðar eftir sitati? Tak tað róligt",
"status.quote_error.unauthorized": "Hesin posturin kann ikki vísast, tí tú hevur ikki rættindi at síggja hann.", "status.quote_post_author": "Siteraði ein post hjá @{name}",
"status.quote_post_author": "Postur hjá @{name}",
"status.read_more": "Les meira", "status.read_more": "Les meira",
"status.reblog": "Stimbra", "status.reblog": "Stimbra",
"status.reblog_private": "Stimbra við upprunasýni", "status.reblog_private": "Stimbra við upprunasýni",
@ -893,6 +899,7 @@
"status.reply": "Svara", "status.reply": "Svara",
"status.replyAll": "Svara tráðnum", "status.replyAll": "Svara tráðnum",
"status.report": "Melda @{name}", "status.report": "Melda @{name}",
"status.revoke_quote": "Strika postin hjá mær frá postinum hjá @{name}",
"status.sensitive_warning": "Viðkvæmt tilfar", "status.sensitive_warning": "Viðkvæmt tilfar",
"status.share": "Deil", "status.share": "Deil",
"status.show_less_all": "Vís øllum minni", "status.show_less_all": "Vís øllum minni",

View File

@ -423,8 +423,6 @@
"hints.profiles.see_more_followers": "Afficher plus d'abonné·e·s sur {domain}", "hints.profiles.see_more_followers": "Afficher plus d'abonné·e·s sur {domain}",
"hints.profiles.see_more_follows": "Afficher plus d'abonné·e·s sur {domain}", "hints.profiles.see_more_follows": "Afficher plus d'abonné·e·s sur {domain}",
"hints.profiles.see_more_posts": "Voir plus de messages sur {domain}", "hints.profiles.see_more_posts": "Voir plus de messages sur {domain}",
"hints.threads.replies_may_be_missing": "Les réponses provenant des autres serveurs pourraient être manquantes.",
"hints.threads.see_more": "Afficher plus de réponses sur {domain}",
"home.column_settings.show_quotes": "Afficher les citations", "home.column_settings.show_quotes": "Afficher les citations",
"home.column_settings.show_reblogs": "Afficher boosts", "home.column_settings.show_reblogs": "Afficher boosts",
"home.column_settings.show_replies": "Afficher réponses", "home.column_settings.show_replies": "Afficher réponses",
@ -866,9 +864,6 @@
"status.mute_conversation": "Masquer la conversation", "status.mute_conversation": "Masquer la conversation",
"status.open": "Afficher la publication entière", "status.open": "Afficher la publication entière",
"status.pin": "Épingler sur profil", "status.pin": "Épingler sur profil",
"status.quote_error.removed": "Ce message a été retiré par son auteur·ice.",
"status.quote_error.unauthorized": "Ce message ne peut pas être affiché car vous n'êtes pas autorisé·e à le voir.",
"status.quote_post_author": "Message par {name}",
"status.read_more": "En savoir plus", "status.read_more": "En savoir plus",
"status.reblog": "Booster", "status.reblog": "Booster",
"status.reblog_private": "Booster avec visibilité originale", "status.reblog_private": "Booster avec visibilité originale",

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