Merge branch 'main' of github.com:glitch-soc/mastodon into main

This commit is contained in:
Holly 2022-02-12 17:23:27 +00:00
commit 35647cbcfe
385 changed files with 14035 additions and 6964 deletions

View File

@ -11,6 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: docker/setup-qemu-action@v1
- uses: docker/setup-buildx-action@v1
- uses: docker/login-action@v1
with:
@ -29,6 +30,7 @@ jobs:
- uses: docker/build-push-action@v2
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/mastodon:latest

View File

@ -5,10 +5,12 @@ All notable changes to this project will be documented in this file.
## [3.4.6] - 2022-02-03
### Fixed
- Fix `mastodon:webpush:generate_vapid_key` task requiring a functional environment ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17338))
- Fix spurious errors when receiving an Add activity for a private post ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17425))
### Security
- Fix error-prone SQL queries ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15828))
- Fix not compacting incoming signed JSON-LD activities ([puckipedia](https://github.com/mastodon/mastodon/pull/17426), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17428)) (CVE-2022-24307)
- Fix insufficient sanitization of report comments ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17430))
@ -17,10 +19,12 @@ All notable changes to this project will be documented in this file.
## [3.4.5] - 2022-01-31
### Added
- Add more advanced migration tests ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17393))
- Add github workflow to build Docker images ([unasuke](https://github.com/mastodon/mastodon/pull/16973), [Gargron](https://github.com/mastodon/mastodon/pull/16980), [Gargron](https://github.com/mastodon/mastodon/pull/17000))
### Fixed
- Fix some old migrations failing when skipping releases ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17394))
- Fix migrations script failing in certain edge cases ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17398))
- Fix Docker build ([tribela](https://github.com/mastodon/mastodon/pull/17188))

10
Gemfile
View File

@ -6,7 +6,7 @@ ruby '>= 2.5.0', '< 3.1.0'
gem 'pkg-config', '~> 1.4'
gem 'rexml', '~> 3.2'
gem 'puma', '~> 5.5'
gem 'puma', '~> 5.6'
gem 'rails', '~> 6.1.4'
gem 'sprockets', '~> 3.7.2'
gem 'thor', '~> 1.2'
@ -18,7 +18,7 @@ gem 'makara', '~> 0.5'
gem 'pghero', '~> 2.8'
gem 'dotenv-rails', '~> 2.7'
gem 'aws-sdk-s3', '~> 1.111', require: false
gem 'aws-sdk-s3', '~> 1.112', require: false
gem 'fog-core', '<= 2.1.0'
gem 'fog-openstack', '~> 0.3', require: false
gem 'kt-paperclip', '~> 7.0'
@ -26,12 +26,10 @@ gem 'blurhash', '~> 0.1'
gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.8'
gem 'bootsnap', '~> 1.10.2', require: false
gem 'bootsnap', '~> 1.10.3', require: false
gem 'browser'
gem 'charlock_holmes', '~> 0.7.7'
gem 'iso-639'
gem 'chewy', '~> 7.2'
gem 'cld3', '~> 3.4.4'
gem 'devise', '~> 4.8'
gem 'devise-two-factor', '~> 4.0'
@ -102,7 +100,7 @@ gem 'rdf-normalize', '~> 0.5'
gem 'redcarpet', '~> 3.5'
group :development, :test do
gem 'fabrication', '~> 2.24'
gem 'fabrication', '~> 2.27'
gem 'fuubar', '~> 2.5'
gem 'i18n-tasks', '~> 0.9', require: false
gem 'pry-byebug', '~> 3.9'

View File

@ -79,17 +79,17 @@ GEM
encryptor (~> 3.0.0)
awrence (1.1.1)
aws-eventstream (1.2.0)
aws-partitions (1.549.0)
aws-sdk-core (3.125.5)
aws-partitions (1.553.0)
aws-sdk-core (3.126.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.53.0)
aws-sdk-core (~> 3, >= 3.125.0)
aws-sdk-kms (1.54.0)
aws-sdk-core (~> 3, >= 3.126.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.111.3)
aws-sdk-core (~> 3, >= 3.125.0)
aws-sdk-s3 (1.112.0)
aws-sdk-core (~> 3, >= 3.126.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.4.0)
@ -104,9 +104,9 @@ GEM
debug_inspector (>= 0.0.1)
blurhash (0.1.5)
ffi (~> 1.14)
bootsnap (1.10.2)
bootsnap (1.10.3)
msgpack (~> 1.2)
brakeman (5.2.0)
brakeman (5.2.1)
browser (4.2.0)
brpoplpush-redis_script (0.1.2)
concurrent-ruby (~> 1.0, >= 1.0.5)
@ -152,8 +152,6 @@ GEM
elasticsearch (>= 7.12.0, < 7.14.0)
elasticsearch-dsl
chunky_png (1.4.0)
cld3 (3.4.4)
ffi (>= 1.1.0, < 1.16.0)
climate_control (0.2.0)
coderay (1.1.3)
color_diff (0.1)
@ -208,10 +206,10 @@ GEM
multi_json
encryptor (3.0.0)
erubi (1.10.0)
et-orbi (1.2.4)
et-orbi (1.2.6)
tzinfo
excon (0.76.0)
fabrication (2.24.0)
fabrication (2.27.0)
faker (2.19.0)
i18n (>= 1.6, < 2)
faraday (1.8.0)
@ -252,7 +250,7 @@ GEM
fog-json (>= 1.0)
ipaddress (>= 0.8)
formatador (0.2.5)
fugit (1.4.5)
fugit (1.5.2)
et-orbi (~> 1.1, >= 1.1.8)
raabro (~> 1.4)
fuubar (2.5.1)
@ -303,7 +301,6 @@ GEM
terminal-table (>= 1.5.1)
idn-ruby (0.1.4)
ipaddress (0.8.3)
iso-639 (0.3.5)
jmespath (1.5.0)
json (2.5.1)
json-canonicalization (0.3.0)
@ -410,14 +407,14 @@ GEM
openssl (2.2.0)
openssl-signature_algorithm (0.4.0)
orm_adapter (0.5.0)
ox (2.14.6)
ox (2.14.7)
parallel (1.21.0)
parser (3.1.0.0)
ast (~> 2.4.1)
parslet (2.0.0)
pastel (0.8.0)
tty-color (~> 0.5)
pg (1.3.0)
pg (1.3.1)
pghero (2.8.2)
activerecord (>= 5)
pkg-config (1.4.7)
@ -439,7 +436,7 @@ GEM
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.6)
puma (5.5.2)
puma (5.6.2)
nio4r (~> 2.0)
pundit (2.1.1)
activesupport (>= 3.0.0)
@ -534,7 +531,7 @@ GEM
rspec-support (3.10.3)
rspec_junit_formatter (0.5.1)
rspec-core (>= 2, < 4, != 2.12.0)
rubocop (1.25.0)
rubocop (1.25.1)
parallel (~> 1.10)
parser (>= 3.1.0.0)
rainbow (>= 2.2.2, < 4.0)
@ -554,7 +551,7 @@ GEM
nokogiri (>= 1.10.5)
rexml
ruby2_keywords (0.0.5)
rufus-scheduler (3.7.0)
rufus-scheduler (3.8.1)
fugit (~> 1.1, >= 1.1.6)
safety_net_attestation (0.4.0)
jwt (~> 2.0)
@ -566,20 +563,20 @@ GEM
railties (>= 4.0.0)
securecompare (1.0.0)
semantic_range (3.0.0)
sidekiq (6.4.0)
sidekiq (6.4.1)
connection_pool (>= 2.2.2)
rack (~> 2.0)
redis (>= 4.2.0)
sidekiq-bulk (0.2.0)
sidekiq
sidekiq-scheduler (3.1.0)
sidekiq-scheduler (3.1.1)
e2mmap
redis (>= 3, < 5)
rufus-scheduler (~> 3.2)
sidekiq (>= 3)
thwait
tilt (>= 1.4.0)
sidekiq-unique-jobs (7.1.12)
sidekiq-unique-jobs (7.1.15)
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
concurrent-ruby (~> 1.0, >= 1.0.5)
sidekiq (>= 5.0, < 8.0)
@ -685,11 +682,11 @@ DEPENDENCIES
active_record_query_trace (~> 1.8)
addressable (~> 2.8)
annotate (~> 3.1)
aws-sdk-s3 (~> 1.111)
aws-sdk-s3 (~> 1.112)
better_errors (~> 2.9)
binding_of_caller (~> 1.0)
blurhash (~> 0.1)
bootsnap (~> 1.10.2)
bootsnap (~> 1.10.3)
brakeman (~> 5.2)
browser
bullet (~> 7.0)
@ -701,7 +698,6 @@ DEPENDENCIES
capybara (~> 3.36)
charlock_holmes (~> 0.7.7)
chewy (~> 7.2)
cld3 (~> 3.4.4)
climate_control (~> 0.2)
color_diff (~> 0.1)
concurrent-ruby
@ -713,7 +709,7 @@ DEPENDENCIES
doorkeeper (~> 5.5)
dotenv-rails (~> 2.7)
ed25519 (~> 1.3)
fabrication (~> 2.24)
fabrication (~> 2.27)
faker (~> 2.19)
fast_blank (~> 1.0)
fastimage
@ -729,7 +725,6 @@ DEPENDENCIES
httplog (~> 1.5.0)
i18n-tasks (~> 0.9)
idn-ruby
iso-639
json-ld
json-ld-preloaded (~> 3.2)
kaminari (~> 1.2)
@ -761,7 +756,7 @@ DEPENDENCIES
private_address_check (~> 0.5)
pry-byebug (~> 3.9)
pry-rails (~> 0.3)
puma (~> 5.5)
puma (~> 5.6)
pundit (~> 2.1)
rack (~> 2.2.3)
rack-attack (~> 6.5)

View File

@ -63,15 +63,29 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
end
def next_page
only_other_accounts = !(@replies&.last&.account_id == @account.id && @replies.size == DESCENDANTS_LIMIT)
if only_other_accounts?
# Only consider remote accounts
return nil if @replies.size < DESCENDANTS_LIMIT
account_status_replies_url(
@account,
@status,
page: true,
min_id: only_other_accounts && !only_other_accounts? ? nil : @replies&.last&.id,
only_other_accounts: only_other_accounts
)
account_status_replies_url(
@account,
@status,
page: true,
min_id: @replies&.last&.id,
only_other_accounts: true
)
else
# For now, we're serving only self-replies, but next page might be other accounts
next_only_other_accounts = @replies&.last&.account_id != @account.id || @replies.size < DESCENDANTS_LIMIT
account_status_replies_url(
@account,
@status,
page: true,
min_id: next_only_other_accounts ? nil : @replies&.last&.id,
only_other_accounts: next_only_other_accounts
)
end
end
def page_params

View File

@ -9,7 +9,8 @@ module Admin
def index
authorize :account, :index?
@accounts = RelationshipFilter.new(@account, filter_params).results.page(params[:page]).per(PER_PAGE)
@accounts = RelationshipFilter.new(@account, filter_params).results.includes(:account_stat, user: [:ips, :invite_request]).page(params[:page]).per(PER_PAGE)
@form = Form::AccountBatch.new
end
private

View File

@ -0,0 +1,50 @@
# frozen_string_literal: true
class Admin::Reports::ActionsController < Admin::BaseController
before_action :set_report
def create
authorize @report, :show?
case action_from_button
when 'delete'
status_batch_action = Admin::StatusBatchAction.new(
type: action_from_button,
status_ids: @report.status_ids,
current_account: current_account,
report_id: @report.id,
send_email_notification: !@report.spam?
)
status_batch_action.save!
when 'silence', 'suspend'
account_action = Admin::AccountAction.new(
type: action_from_button,
report_id: @report.id,
target_account: @report.target_account,
current_account: current_account,
send_email_notification: !@report.spam?
)
account_action.save!
end
redirect_to admin_reports_path
end
private
def set_report
@report = Report.find(params[:report_id])
end
def action_from_button
if params[:delete]
'delete'
elsif params[:silence]
'silence'
elsif params[:suspend]
'suspend'
end
end
end

View File

@ -20,7 +20,7 @@ class Api::V1::MediaController < Api::BaseController
end
def update
@media_attachment.update!(media_attachment_params)
@media_attachment.update!(updateable_media_attachment_params)
render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment
end
@ -42,6 +42,10 @@ class Api::V1::MediaController < Api::BaseController
params.permit(:file, :thumbnail, :description, :focus)
end
def updateable_media_attachment_params
params.permit(:thumbnail, :description, :focus)
end
def file_type_error
{ error: 'File type of uploaded media could not be verified' }
end

View File

@ -33,6 +33,6 @@ class Api::V1::ReportsController < Api::BaseController
end
def report_params
params.permit(:account_id, :comment, :forward, status_ids: [])
params.permit(:account_id, :comment, :category, :forward, status_ids: [], rule_ids: [])
end
end

View File

@ -7,7 +7,7 @@ class Api::V1::Statuses::HistoriesController < Api::BaseController
before_action :set_status
def show
render json: @status.edits, each_serializer: REST::StatusEditSerializer
render json: @status.edits.includes(:account, status: [:account]), each_serializer: REST::StatusEditSerializer
end
private

View File

@ -3,8 +3,8 @@
class Api::V1::StatusesController < Api::BaseController
include Authorization
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :destroy]
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :destroy]
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :update, :destroy]
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :update, :destroy]
before_action :require_user!, except: [:show, :context]
before_action :set_status, only: [:show, :context]
before_action :set_thread, only: [:create]
@ -35,25 +35,46 @@ class Api::V1::StatusesController < Api::BaseController
end
def create
@status = PostStatusService.new.call(current_user.account,
text: status_params[:status],
thread: @thread,
media_ids: status_params[:media_ids],
sensitive: status_params[:sensitive],
spoiler_text: status_params[:spoiler_text],
visibility: status_params[:visibility],
scheduled_at: status_params[:scheduled_at],
application: doorkeeper_token.application,
poll: status_params[:poll],
content_type: status_params[:content_type],
idempotency: request.headers['Idempotency-Key'],
with_rate_limit: true)
@status = PostStatusService.new.call(
current_user.account,
text: status_params[:status],
thread: @thread,
media_ids: status_params[:media_ids],
sensitive: status_params[:sensitive],
spoiler_text: status_params[:spoiler_text],
visibility: status_params[:visibility],
language: status_params[:language],
scheduled_at: status_params[:scheduled_at],
application: doorkeeper_token.application,
poll: status_params[:poll],
content_type: status_params[:content_type],
idempotency: request.headers['Idempotency-Key'],
with_rate_limit: true
)
render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
end
def update
@status = Status.where(account: current_account).find(params[:id])
authorize @status, :update?
UpdateStatusService.new.call(
@status,
current_account.id,
text: status_params[:status],
media_ids: status_params[:media_ids],
sensitive: status_params[:sensitive],
spoiler_text: status_params[:spoiler_text],
poll: status_params[:poll],
content_type: status_params[:content_type]
)
render json: @status, serializer: REST::StatusSerializer
end
def destroy
@status = Status.where(account_id: current_user.account).find(params[:id])
@status = Status.where(account: current_account).find(params[:id])
authorize @status, :destroy?
@status.discard
@ -85,6 +106,7 @@ class Api::V1::StatusesController < Api::BaseController
:sensitive,
:spoiler_text,
:visibility,
:language,
:scheduled_at,
:content_type,
media_ids: [],

View File

@ -26,6 +26,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
mention: alerts_enabled,
poll: alerts_enabled,
status: alerts_enabled,
update: alerts_enabled,
},
}
@ -61,6 +62,15 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
end
def data_params
@data_params ||= params.require(:data).permit(:policy, alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll, :status])
@data_params ||= params.require(:data).permit(:policy, alerts: [
:follow,
:follow_request,
:favourite,
:reblog,
:mention,
:poll,
:status,
:update,
])
end
end

View File

@ -136,16 +136,6 @@ class ApplicationController < ActionController::Base
@current_session = SessionActivation.find_by(session_id: cookies.signed['_session_id']) if cookies.signed['_session_id'].present?
end
def current_flavour
return Setting.flavour unless Themes.instance.flavours.include? current_user&.setting_flavour
current_user.setting_flavour
end
def current_skin
return Setting.skin unless Themes.instance.skins_for(current_flavour).include? current_user&.setting_skin
current_user.setting_skin
end
def respond_with_error(code)
respond_to do |format|
format.any do

View File

@ -7,27 +7,24 @@ module Localized
around_action :set_locale
end
def set_locale
locale = current_user.locale if respond_to?(:user_signed_in?) && user_signed_in?
locale ||= session[:locale] ||= default_locale
locale = default_locale unless I18n.available_locales.include?(locale.to_sym)
I18n.with_locale(locale) do
yield
end
def set_locale(&block)
I18n.with_locale(requested_locale || I18n.default_locale, &block)
end
private
def default_locale
if ENV['DEFAULT_LOCALE'].present?
I18n.default_locale
else
request_locale || I18n.default_locale
end
def requested_locale
requested_locale_name = available_locale_or_nil(params[:locale])
requested_locale_name ||= available_locale_or_nil(current_user.locale) if respond_to?(:user_signed_in?) && user_signed_in?
requested_locale_name ||= http_accept_language if ENV['DEFAULT_LOCALE'].blank?
requested_locale_name
end
def request_locale
http_accept_language.language_region_compatible_from(I18n.available_locales)
def http_accept_language
HttpAcceptLanguage::Parser.new(request.headers.fetch('Accept-Language')).language_region_compatible_from(I18n.available_locales) if request.headers.key?('Accept-Language')
end
def available_locale_or_nil(locale_name)
locale_name.to_sym if locale_name.present? && I18n.available_locales.include?(locale_name.to_sym)
end
end

View File

@ -10,8 +10,17 @@ module ThemingConcern
private
def current_flavour
[current_user&.setting_flavour, Setting.flavour, 'glitch', 'vanilla'].find { |flavour| Themes.instance.flavours.include?(flavour) }
end
def current_skin
skins = Themes.instance.skins_for(current_flavour)
[current_user&.setting_skin, Setting.skin, 'default'].find { |skin| skins.include?(skin) }
end
def valid_pack_data?(data, pack_name)
data['pack'].is_a?(Hash) && [String, Hash].any? { |c| data['pack'][pack_name].is_a?(c) }
data['pack'].is_a?(Hash) && data['pack'][pack_name].present?
end
def nil_pack(data)

View File

@ -3,6 +3,7 @@
class InstanceActorsController < ApplicationController
include AccountControllerConcern
skip_before_action :check_account_confirmation
skip_around_action :set_locale
def show

View File

@ -1,94 +1,247 @@
# frozen_string_literal: true
module LanguagesHelper
HUMAN_LOCALES = {
af: 'Afrikaans',
ar: 'العربية',
ast: 'Asturianu',
bg: 'Български',
bn: 'বাংলা',
br: 'Breton',
ca: 'Català',
co: 'Corsu',
cs: 'Čeština',
cy: 'Cymraeg',
da: 'Dansk',
de: 'Deutsch',
el: 'Ελληνικά',
en: 'English',
eo: 'Esperanto',
ISO_639_1 = {
aa: ['Afar', 'Afaraf'].freeze,
ab: ['Abkhaz', 'аҧсуа бызшәа'].freeze,
ae: ['Avestan', 'avesta'].freeze,
af: ['Afrikaans', 'Afrikaans'].freeze,
ak: ['Akan', 'Akan'].freeze,
am: ['Amharic', 'አማርኛ'].freeze,
an: ['Aragonese', 'aragonés'].freeze,
ar: ['Arabic', 'اللغة العربية'].freeze,
as: ['Assamese', 'অসমীয়া'].freeze,
av: ['Avaric', 'авар мацӀ'].freeze,
ay: ['Aymara', 'aymar aru'].freeze,
az: ['Azerbaijani', 'azərbaycan dili'].freeze,
ba: ['Bashkir', 'башҡорт теле'].freeze,
be: ['Belarusian', 'беларуская мова'].freeze,
bg: ['Bulgarian', 'български език'].freeze,
bh: ['Bihari', 'भोजपुरी'].freeze,
bi: ['Bislama', 'Bislama'].freeze,
bm: ['Bambara', 'bamanankan'].freeze,
bn: ['Bengali', 'বাংলা'].freeze,
bo: ['Tibetan', 'བོད་ཡིག'].freeze,
br: ['Breton', 'brezhoneg'].freeze,
bs: ['Bosnian', 'bosanski jezik'].freeze,
ca: ['Catalan', 'Català'].freeze,
ce: ['Chechen', 'нохчийн мотт'].freeze,
ch: ['Chamorro', 'Chamoru'].freeze,
co: ['Corsican', 'corsu'].freeze,
cr: ['Cree', 'ᓀᐦᐃᔭᐍᐏᐣ'].freeze,
cs: ['Czech', 'čeština'].freeze,
cu: ['Old Church Slavonic', 'ѩзыкъ словѣньскъ'].freeze,
cv: ['Chuvash', 'чӑваш чӗлхи'].freeze,
cy: ['Welsh', 'Cymraeg'].freeze,
da: ['Danish', 'dansk'].freeze,
de: ['German', 'Deutsch'].freeze,
dv: ['Divehi', 'Dhivehi'].freeze,
dz: ['Dzongkha', 'རྫོང་ཁ'].freeze,
ee: ['Ewe', 'Eʋegbe'].freeze,
el: ['Greek', 'Ελληνικά'].freeze,
en: ['English', 'English'].freeze,
eo: ['Esperanto', 'Esperanto'].freeze,
es: ['Spanish', 'Español'].freeze,
et: ['Estonian', 'eesti'].freeze,
eu: ['Basque', 'euskara'].freeze,
fa: ['Persian', 'فارسی'].freeze,
ff: ['Fula', 'Fulfulde'].freeze,
fi: ['Finnish', 'suomi'].freeze,
fj: ['Fijian', 'Vakaviti'].freeze,
fo: ['Faroese', 'føroyskt'].freeze,
fr: ['French', 'Français'].freeze,
fy: ['Western Frisian', 'Frysk'].freeze,
ga: ['Irish', 'Gaeilge'].freeze,
gd: ['Scottish Gaelic', 'Gàidhlig'].freeze,
gl: ['Galician', 'galego'].freeze,
gu: ['Gujarati', 'ગુજરાતી'].freeze,
gv: ['Manx', 'Gaelg'].freeze,
ha: ['Hausa', 'هَوُسَ'].freeze,
he: ['Hebrew', 'עברית'].freeze,
hi: ['Hindi', 'हिन्दी'].freeze,
ho: ['Hiri Motu', 'Hiri Motu'].freeze,
hr: ['Croatian', 'Hrvatski'].freeze,
ht: ['Haitian', 'Kreyòl ayisyen'].freeze,
hu: ['Hungarian', 'magyar'].freeze,
hy: ['Armenian', 'Հայերեն'].freeze,
hz: ['Herero', 'Otjiherero'].freeze,
ia: ['Interlingua', 'Interlingua'].freeze,
id: ['Indonesian', 'Bahasa Indonesia'].freeze,
ie: ['Interlingue', 'Interlingue'].freeze,
ig: ['Igbo', 'Asụsụ Igbo'].freeze,
ii: ['Nuosu', 'ꆈꌠ꒿ Nuosuhxop'].freeze,
ik: ['Inupiaq', 'Iñupiaq'].freeze,
io: ['Ido', 'Ido'].freeze,
is: ['Icelandic', 'Íslenska'].freeze,
it: ['Italian', 'Italiano'].freeze,
iu: ['Inuktitut', 'ᐃᓄᒃᑎᑐᑦ'].freeze,
ja: ['Japanese', '日本語'].freeze,
jv: ['Javanese', 'basa Jawa'].freeze,
ka: ['Georgian', 'ქართული'].freeze,
kg: ['Kongo', 'Kikongo'].freeze,
ki: ['Kikuyu', 'Gĩkũyũ'].freeze,
kj: ['Kwanyama', 'Kuanyama'].freeze,
kk: ['Kazakh', 'қазақ тілі'].freeze,
kl: ['Kalaallisut', 'kalaallisut'].freeze,
km: ['Khmer', 'ខេមរភាសា'].freeze,
kn: ['Kannada', 'ಕನ್ನಡ'].freeze,
ko: ['Korean', '한국어'].freeze,
kr: ['Kanuri', 'Kanuri'].freeze,
ks: ['Kashmiri', 'कश्मीरी'].freeze,
ku: ['Kurdish', 'Kurdî'].freeze,
kv: ['Komi', 'коми кыв'].freeze,
kw: ['Cornish', 'Kernewek'].freeze,
ky: ['Kyrgyz', 'Кыргызча'].freeze,
la: ['Latin', 'latine'].freeze,
lb: ['Luxembourgish', 'Lëtzebuergesch'].freeze,
lg: ['Ganda', 'Luganda'].freeze,
li: ['Limburgish', 'Limburgs'].freeze,
ln: ['Lingala', 'Lingála'].freeze,
lo: ['Lao', 'ພາສາ'].freeze,
lt: ['Lithuanian', 'lietuvių kalba'].freeze,
lu: ['Luba-Katanga', 'Tshiluba'].freeze,
lv: ['Latvian', 'latviešu valoda'].freeze,
mg: ['Malagasy', 'fiteny malagasy'].freeze,
mh: ['Marshallese', 'Kajin M̧ajeļ'].freeze,
mi: ['Māori', 'te reo Māori'].freeze,
mk: ['Macedonian', 'македонски јазик'].freeze,
ml: ['Malayalam', 'മലയാളം'].freeze,
mn: ['Mongolian', 'Монгол хэл'].freeze,
mr: ['Marathi', 'मराठी'].freeze,
ms: ['Malay', 'Bahasa Malaysia'].freeze,
mt: ['Maltese', 'Malti'].freeze,
my: ['Burmese', 'ဗမာစာ'].freeze,
na: ['Nauru', 'Ekakairũ Naoero'].freeze,
nb: ['Norwegian Bokmål', 'Norsk bokmål'].freeze,
nd: ['Northern Ndebele', 'isiNdebele'].freeze,
ne: ['Nepali', 'नेपाली'].freeze,
ng: ['Ndonga', 'Owambo'].freeze,
nl: ['Dutch', 'Nederlands'].freeze,
nn: ['Norwegian Nynorsk', 'Norsk nynorsk'].freeze,
no: ['Norwegian', 'Norsk'].freeze,
nr: ['Southern Ndebele', 'isiNdebele'].freeze,
nv: ['Navajo', 'Diné bizaad'].freeze,
ny: ['Chichewa', 'chiCheŵa'].freeze,
oc: ['Occitan', 'occitan'].freeze,
oj: ['Ojibwe', 'ᐊᓂᔑᓈᐯᒧᐎᓐ'].freeze,
om: ['Oromo', 'Afaan Oromoo'].freeze,
or: ['Oriya', 'ଓଡ଼ିଆ'].freeze,
os: ['Ossetian', 'ирон æвзаг'].freeze,
pa: ['Panjabi', 'ਪੰਜਾਬੀ'].freeze,
pi: ['Pāli', 'पाऴि'].freeze,
pl: ['Polish', 'Polski'].freeze,
ps: ['Pashto', 'پښتو'].freeze,
pt: ['Portuguese', 'Português'].freeze,
qu: ['Quechua', 'Runa Simi'].freeze,
rm: ['Romansh', 'rumantsch grischun'].freeze,
rn: ['Kirundi', 'Ikirundi'].freeze,
ro: ['Romanian', 'Română'].freeze,
ru: ['Russian', 'Русский'].freeze,
rw: ['Kinyarwanda', 'Ikinyarwanda'].freeze,
sa: ['Sanskrit', 'संस्कृतम्'].freeze,
sc: ['Sardinian', 'sardu'].freeze,
sd: ['Sindhi', 'सिन्धी'].freeze,
se: ['Northern Sami', 'Davvisámegiella'].freeze,
sg: ['Sango', 'yângâ tî sängö'].freeze,
si: ['Sinhala', 'සිංහල'].freeze,
sk: ['Slovak', 'slovenčina'].freeze,
sl: ['Slovenian', 'slovenščina'].freeze,
sn: ['Shona', 'chiShona'].freeze,
so: ['Somali', 'Soomaaliga'].freeze,
sq: ['Albanian', 'Shqip'].freeze,
sr: ['Serbian', 'српски језик'].freeze,
ss: ['Swati', 'SiSwati'].freeze,
st: ['Southern Sotho', 'Sesotho'].freeze,
su: ['Sundanese', 'Basa Sunda'].freeze,
sv: ['Swedish', 'Svenska'].freeze,
sw: ['Swahili', 'Kiswahili'].freeze,
ta: ['Tamil', 'தமிழ்'].freeze,
te: ['Telugu', 'తెలుగు'].freeze,
tg: ['Tajik', 'тоҷикӣ'].freeze,
th: ['Thai', 'ไทย'].freeze,
ti: ['Tigrinya', 'ትግርኛ'].freeze,
tk: ['Turkmen', 'Türkmen'].freeze,
tl: ['Tagalog', 'Wikang Tagalog'].freeze,
tn: ['Tswana', 'Setswana'].freeze,
to: ['Tonga', 'faka Tonga'].freeze,
tr: ['Turkish', 'Türkçe'].freeze,
ts: ['Tsonga', 'Xitsonga'].freeze,
tt: ['Tatar', 'татар теле'].freeze,
tw: ['Twi', 'Twi'].freeze,
ty: ['Tahitian', 'Reo Tahiti'].freeze,
ug: ['Uyghur', 'ئۇيغۇرچە‎'].freeze,
uk: ['Ukrainian', 'Українська'].freeze,
ur: ['Urdu', 'اردو'].freeze,
uz: ['Uzbek', 'Ўзбек'].freeze,
ve: ['Venda', 'Tshivenḓa'].freeze,
vi: ['Vietnamese', 'Tiếng Việt'].freeze,
vo: ['Volapük', 'Volapük'].freeze,
wa: ['Walloon', 'walon'].freeze,
wo: ['Wolof', 'Wollof'].freeze,
xh: ['Xhosa', 'isiXhosa'].freeze,
yi: ['Yiddish', 'ייִדיש'].freeze,
yo: ['Yoruba', 'Yorùbá'].freeze,
za: ['Zhuang', 'Saɯ cueŋƅ'].freeze,
zh: ['Chinese', '中文'].freeze,
zu: ['Zulu', 'isiZulu'].freeze,
}.freeze
ISO_639_3 = {
ast: ['Asturian', 'Asturianu'].freeze,
kab: ['Kabyle', 'Taqbaylit'].freeze,
kmr: ['Northern Kurdish', 'Kurmancî'].freeze,
zgh: ['Standard Moroccan Tamazight', 'ⵜⴰⵎⴰⵣⵉⵖⵜ'].freeze,
}.freeze
SUPPORTED_LOCALES = {}.merge(ISO_639_1).merge(ISO_639_3).freeze
# For ISO-639-1 and ISO-639-3 language codes, we have their official
# names, but for some translations, we need the names of the
# regional variants specifically
REGIONAL_LOCALE_NAMES = {
'es-AR': 'Español (Argentina)',
'es-MX': 'Español (México)',
es: 'Español',
et: 'Eesti',
eu: 'Euskara',
fa: 'فارسی',
fi: 'Suomi',
fr: 'Français',
ga: 'Gaeilge',
gd: 'Gàidhlig',
gl: 'Galego',
he: 'עברית',
hi: 'हिन्दी',
hr: 'Hrvatski',
hu: 'Magyar',
hy: 'Հայերեն',
id: 'Bahasa Indonesia',
io: 'Ido',
is: 'Íslenska',
it: 'Italiano',
ja: '日本語',
ka: 'ქართული',
kab: 'Taqbaylit',
kk: 'Қазақша',
kmr: 'Kurmancî',
kn: 'ಕನ್ನಡ',
ko: '한국어',
ku: 'سۆرانی',
lt: 'Lietuvių',
lv: 'Latviešu',
mk: 'Македонски',
ml: 'മലയാളം',
mr: 'मराठी',
ms: 'Bahasa Melayu',
nl: 'Nederlands',
nn: 'Nynorsk',
no: 'Norsk',
oc: 'Occitan',
pl: 'Polski',
'pt-BR': 'Português (Brasil)',
'pt-PT': 'Português (Portugal)',
pt: 'Português',
ro: 'Română',
ru: 'Русский',
sa: 'संस्कृतम्',
sc: 'Sardu',
si: 'සිංහල',
sk: 'Slovenčina',
sl: 'Slovenščina',
sq: 'Shqip',
'sr-Latn': 'Srpski (latinica)',
sr: 'Српски',
sv: 'Svenska',
ta: 'தமிழ்',
te: 'తెలుగు',
th: 'ไทย',
tr: 'Türkçe',
uk: 'Українська',
ur: 'اُردُو',
vi: 'Tiếng Việt',
zgh: 'ⵜⴰⵎⴰⵣⵉⵖⵜ',
'zh-CN': '简体中文',
'zh-HK': '繁體中文(香港)',
'zh-TW': '繁體中文(臺灣)',
zh: '中文',
}.freeze
def human_locale(locale)
if locale == 'und'
def native_locale_name(locale)
if locale.blank? || locale == 'und'
I18n.t('generic.none')
elsif (supported_locale = SUPPORTED_LOCALES[locale.to_sym])
supported_locale[1]
elsif (regional_locale = REGIONAL_LOCALE_NAMES[locale.to_sym])
regional_locale
else
HUMAN_LOCALES[locale.to_sym] || locale
locale
end
end
def standard_locale_name(locale)
if locale.blank?
I18n.t('generic.none')
elsif (supported_locale = SUPPORTED_LOCALES[locale.to_sym])
supported_locale[0]
else
locale
end
end
def valid_locale_or_nil(str)
return if str.blank?
code, = str.to_s.split(/[_-]/) # Strip out the region from e.g. en_US or ja-JP
return unless valid_locale?(code)
code
end
def valid_locale?(locale)
SUPPORTED_LOCALES.key?(locale.to_sym)
end
end

View File

@ -2,7 +2,7 @@
module SettingsHelper
def filterable_languages
LanguageDetector.instance.language_names.select(&LanguagesHelper::HUMAN_LOCALES.method(:key?))
LanguagesHelper::SUPPORTED_LOCALES.keys
end
def hash_to_object(hash)

View File

@ -75,6 +75,8 @@ export const INIT_MEDIA_EDIT_MODAL = 'INIT_MEDIA_EDIT_MODAL';
export const COMPOSE_CHANGE_MEDIA_DESCRIPTION = 'COMPOSE_CHANGE_MEDIA_DESCRIPTION';
export const COMPOSE_CHANGE_MEDIA_FOCUS = 'COMPOSE_CHANGE_MEDIA_FOCUS';
export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS';
const messages = defineMessages({
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
@ -88,6 +90,15 @@ export const ensureComposeIsVisible = (getState, routerHistory) => {
}
};
export function setComposeToStatus(status, text, spoiler_text) {
return{
type: COMPOSE_SET_STATUS,
status,
text,
spoiler_text,
};
};
export function changeCompose(text) {
return {
type: COMPOSE_CHANGE,
@ -150,8 +161,9 @@ export function directCompose(account, routerHistory) {
export function submitCompose(routerHistory) {
return function (dispatch, getState) {
let status = getState().getIn(['compose', 'text'], '');
let media = getState().getIn(['compose', 'media_attachments']);
let status = getState().getIn(['compose', 'text'], '');
const media = getState().getIn(['compose', 'media_attachments']);
const statusId = getState().getIn(['compose', 'id'], null);
const spoilers = getState().getIn(['compose', 'spoiler']) || getState().getIn(['local_settings', 'always_show_spoilers_field']);
let spoilerText = spoilers ? getState().getIn(['compose', 'spoiler_text'], '') : '';
@ -159,20 +171,25 @@ export function submitCompose(routerHistory) {
return;
}
dispatch(submitComposeRequest());
if (getState().getIn(['compose', 'advanced_options', 'do_not_federate'])) {
status = status + ' 👁️';
}
api(getState).post('/api/v1/statuses', {
status,
content_type: getState().getIn(['compose', 'content_type']),
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
media_ids: media.map(item => item.get('id')),
sensitive: getState().getIn(['compose', 'sensitive']) || (spoilerText.length > 0 && media.size !== 0),
spoiler_text: spoilerText,
visibility: getState().getIn(['compose', 'privacy']),
poll: getState().getIn(['compose', 'poll'], null),
}, {
dispatch(submitComposeRequest());
api(getState).request({
url: statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`,
method: statusId === null ? 'post' : 'put',
data: {
status,
content_type: getState().getIn(['compose', 'content_type']),
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
media_ids: media.map(item => item.get('id')),
sensitive: getState().getIn(['compose', 'sensitive']) || (spoilerText.length > 0 && media.size !== 0),
spoiler_text: spoilerText,
visibility: getState().getIn(['compose', 'privacy']),
poll: getState().getIn(['compose', 'poll'], null),
},
headers: {
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
},
@ -202,14 +219,16 @@ export function submitCompose(routerHistory) {
}
};
insertIfOnline('home');
if (statusId === null) {
insertIfOnline('home');
}
if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
if (statusId === null && response.data.in_reply_to_id === null && response.data.visibility === 'public') {
insertIfOnline('community');
if (!response.data.local_only) {
insertIfOnline('public');
}
} else if (response.data.visibility === 'direct') {
} else if (statusId === null && response.data.visibility === 'direct') {
insertIfOnline('direct');
}
}).catch(function (error) {

View File

@ -0,0 +1,37 @@
import api from 'flavours/glitch/util/api';
import { importFetchedAccounts } from './importer';
export const HISTORY_FETCH_REQUEST = 'HISTORY_FETCH_REQUEST';
export const HISTORY_FETCH_SUCCESS = 'HISTORY_FETCH_SUCCESS';
export const HISTORY_FETCH_FAIL = 'HISTORY_FETCH_FAIL';
export const fetchHistory = statusId => (dispatch, getState) => {
const loading = getState().getIn(['history', statusId, 'loading']);
if (loading) {
return;
}
dispatch(fetchHistoryRequest(statusId));
api(getState).get(`/api/v1/statuses/${statusId}/history`).then(({ data }) => {
dispatch(importFetchedAccounts(data.map(x => x.account)));
dispatch(fetchHistorySuccess(statusId, data));
}).catch(error => dispatch(fetchHistoryFail(error)));
};
export const fetchHistoryRequest = statusId => ({
type: HISTORY_FETCH_REQUEST,
statusId,
});
export const fetchHistorySuccess = (statusId, history) => ({
type: HISTORY_FETCH_SUCCESS,
statusId,
history,
});
export const fetchHistoryFail = error => ({
type: HISTORY_FETCH_FAIL,
error,
});

View File

@ -47,7 +47,6 @@ export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT';
export const NOTIFICATIONS_SET_VISIBILITY = 'NOTIFICATIONS_SET_VISIBILITY';
export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ';
export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT';
@ -136,7 +135,17 @@ const excludeTypesFromSettings = state => state.getIn(['settings', 'notification
const excludeTypesFromFilter = filter => {
const allTypes = ImmutableList(['follow', 'follow_request', 'favourite', 'reblog', 'mention', 'poll']);
const allTypes = ImmutableList([
'follow',
'follow_request',
'favourite',
'reblog',
'mention',
'poll',
'status',
'update',
]);
return allTypes.filterNot(item => item === filter).toJS();
};

View File

@ -2,7 +2,7 @@ import api from 'flavours/glitch/util/api';
import { deleteFromTimelines } from './timelines';
import { importFetchedStatus, importFetchedStatuses } from './importer';
import { ensureComposeIsVisible } from './compose';
import { ensureComposeIsVisible, setComposeToStatus } from './compose';
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
@ -26,6 +26,10 @@ export const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL';
export const REDRAFT = 'REDRAFT';
export const STATUS_FETCH_SOURCE_REQUEST = 'STATUS_FETCH_SOURCE_REQUEST';
export const STATUS_FETCH_SOURCE_SUCCESS = 'STATUS_FETCH_SOURCE_SUCCESS';
export const STATUS_FETCH_SOURCE_FAIL = 'STATUS_FETCH_SOURCE_FAIL';
export function fetchStatusRequest(id, skipLoading) {
return {
type: STATUS_FETCH_REQUEST,
@ -81,6 +85,37 @@ export function redraft(status, raw_text, content_type) {
};
};
export const editStatus = (id, routerHistory) => (dispatch, getState) => {
let status = getState().getIn(['statuses', id]);
if (status.get('poll')) {
status = status.set('poll', getState().getIn(['polls', status.get('poll')]));
}
dispatch(fetchStatusSourceRequest());
api(getState).get(`/api/v1/statuses/${id}/source`).then(response => {
dispatch(fetchStatusSourceSuccess());
ensureComposeIsVisible(getState, routerHistory);
dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text));
}).catch(error => {
dispatch(fetchStatusSourceFail(error));
});
};
export const fetchStatusSourceRequest = () => ({
type: STATUS_FETCH_SOURCE_REQUEST,
});
export const fetchStatusSourceSuccess = () => ({
type: STATUS_FETCH_SOURCE_SUCCESS,
});
export const fetchStatusSourceFail = error => ({
type: STATUS_FETCH_SOURCE_FAIL,
error,
});
export function deleteStatus(id, routerHistory, withRedraft = false) {
return (dispatch, getState) => {
let status = getState().getIn(['statuses', id]);

View File

@ -6,6 +6,8 @@ import Overlay from 'react-overlays/lib/Overlay';
import Motion from 'flavours/glitch/util/optional_motion';
import spring from 'react-motion/lib/spring';
import { supportsPassiveEvents } from 'detect-passive-events';
import classNames from 'classnames';
import { CircularProgress } from 'mastodon/components/loading_indicator';
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
let id = 0;
@ -17,13 +19,18 @@ class DropdownMenu extends React.PureComponent {
};
static propTypes = {
items: PropTypes.array.isRequired,
items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired,
loading: PropTypes.bool,
scrollable: PropTypes.bool,
onClose: PropTypes.func.isRequired,
style: PropTypes.object,
placement: PropTypes.string,
arrowOffsetLeft: PropTypes.string,
arrowOffsetTop: PropTypes.string,
openedViaKeyboard: PropTypes.bool,
renderItem: PropTypes.func,
renderHeader: PropTypes.func,
onItemClick: PropTypes.func.isRequired,
};
static defaultProps = {
@ -45,9 +52,11 @@ class DropdownMenu extends React.PureComponent {
document.addEventListener('click', this.handleDocumentClick, false);
document.addEventListener('keydown', this.handleKeyDown, false);
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
if (this.focusedItem && this.props.openedViaKeyboard) {
this.focusedItem.focus({ preventScroll: true });
}
this.setState({ mounted: true });
}
@ -66,7 +75,7 @@ class DropdownMenu extends React.PureComponent {
}
handleKeyDown = e => {
const items = Array.from(this.node.getElementsByTagName('a'));
const items = Array.from(this.node.querySelectorAll('a, button'));
const index = items.indexOf(document.activeElement);
let element = null;
@ -109,30 +118,20 @@ class DropdownMenu extends React.PureComponent {
}
handleClick = e => {
const i = Number(e.currentTarget.getAttribute('data-index'));
const { action, to } = this.props.items[i];
this.props.onClose();
if (typeof action === 'function') {
e.preventDefault();
action();
} else if (to) {
e.preventDefault();
this.context.router.history.push(to);
}
const { onItemClick } = this.props;
onItemClick(e);
}
renderItem (option, i) {
renderItem = (option, i) => {
if (option === null) {
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
}
const { text, href = '#' } = option;
const { text, href = '#', target = '_blank', method } = option;
return (
<li className='dropdown-menu__item' key={`${text}-${i}`}>
<a href={href} target='_blank' rel='noopener noreferrer' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}>
<a href={href} target={target} data-method={method} rel='noopener noreferrer' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}>
{text}
</a>
</li>
@ -140,21 +139,37 @@ class DropdownMenu extends React.PureComponent {
}
render () {
const { items, style, placement, arrowOffsetLeft, arrowOffsetTop } = this.props;
const { items, style, placement, arrowOffsetLeft, arrowOffsetTop, scrollable, renderHeader, loading } = this.props;
const { mounted } = this.state;
let renderItem = this.props.renderItem || this.renderItem;
return (
<Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
{({ opacity, scaleX, scaleY }) => (
// It should not be transformed when mounting because the resulting
// size will be used to determine the coordinate of the menu by
// react-overlays
<div className='dropdown-menu' style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}>
<div className={`dropdown-menu ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}>
<div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} />
<ul>
{items.map((option, i) => this.renderItem(option, i))}
</ul>
<div className={classNames('dropdown-menu__container', { 'dropdown-menu__container--loading': loading })}>
{loading && (
<CircularProgress size={30} strokeWidth={3.5} />
)}
{!loading && renderHeader && (
<div className='dropdown-menu__container__header'>
{renderHeader(items)}
</div>
)}
{!loading && (
<ul className={classNames('dropdown-menu__container__list', { 'dropdown-menu__container__list--scrollable': scrollable })}>
{items.map((option, i) => renderItem(option, i, { onClick: this.handleClick, onKeyPress: this.handleItemKeyPress }))}
</ul>
)}
</div>
</div>
)}
</Motion>
@ -170,11 +185,14 @@ export default class Dropdown extends React.PureComponent {
};
static propTypes = {
icon: PropTypes.string.isRequired,
items: PropTypes.array.isRequired,
size: PropTypes.number.isRequired,
children: PropTypes.node,
icon: PropTypes.string,
items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired,
loading: PropTypes.bool,
size: PropTypes.number,
title: PropTypes.string,
disabled: PropTypes.bool,
scrollable: PropTypes.bool,
status: ImmutablePropTypes.map,
isUserTouching: PropTypes.func,
onOpen: PropTypes.func.isRequired,
@ -182,6 +200,9 @@ export default class Dropdown extends React.PureComponent {
dropdownPlacement: PropTypes.string,
openDropdownId: PropTypes.number,
openedViaKeyboard: PropTypes.bool,
renderItem: PropTypes.func,
renderHeader: PropTypes.func,
onItemClick: PropTypes.func,
};
static defaultProps = {
@ -236,17 +257,22 @@ export default class Dropdown extends React.PureComponent {
}
}
handleItemClick = (i, e) => {
const { action, to } = this.props.items[i];
handleItemClick = e => {
const { onItemClick } = this.props;
const i = Number(e.currentTarget.getAttribute('data-index'));
const item = this.props.items[i];
this.handleClose();
if (typeof action === 'function') {
if (typeof onItemClick === 'function') {
e.preventDefault();
action();
} else if (to) {
onItemClick(item, i);
} else if (item && typeof item.action === 'function') {
e.preventDefault();
this.context.router.history.push(to);
item.action();
} else if (item && item.to) {
e.preventDefault();
this.context.router.history.push(item.to);
}
}
@ -264,29 +290,67 @@ export default class Dropdown extends React.PureComponent {
}
}
close = () => {
this.handleClose();
}
render () {
const { icon, items, size, title, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard } = this.props;
const {
icon,
items,
size,
title,
disabled,
loading,
scrollable,
dropdownPlacement,
openDropdownId,
openedViaKeyboard,
children,
renderItem,
renderHeader,
} = this.props;
const open = this.state.id === openDropdownId;
const button = children ? React.cloneElement(React.Children.only(children), {
ref: this.setTargetRef,
onClick: this.handleClick,
onMouseDown: this.handleMouseDown,
onKeyDown: this.handleButtonKeyDown,
onKeyPress: this.handleKeyPress,
}) : (
<IconButton
icon={icon}
title={title}
active={open}
disabled={disabled}
size={size}
ref={this.setTargetRef}
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
onKeyDown={this.handleButtonKeyDown}
onKeyPress={this.handleKeyPress}
/>
);
return (
<div>
<IconButton
icon={icon}
title={title}
active={open}
disabled={disabled}
size={size}
ref={this.setTargetRef}
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
onKeyDown={this.handleButtonKeyDown}
onKeyPress={this.handleKeyPress}
/>
<React.Fragment>
{button}
<Overlay show={open} placement={dropdownPlacement} target={this.findTarget}>
<DropdownMenu items={items} onClose={this.handleClose} openedViaKeyboard={openedViaKeyboard} />
<DropdownMenu
items={items}
loading={loading}
scrollable={scrollable}
onClose={this.handleClose}
openedViaKeyboard={openedViaKeyboard}
renderItem={renderItem}
renderHeader={renderHeader}
onItemClick={this.handleItemClick}
/>
</Overlay>
</div>
</React.Fragment>
);
}

View File

@ -0,0 +1,27 @@
import { connect } from 'react-redux';
import { openDropdownMenu, closeDropdownMenu } from 'flavours/glitch/actions/dropdown_menu';
import { fetchHistory } from 'flavours/glitch/actions/history';
import DropdownMenu from 'flavours/glitch/components/dropdown_menu';
const mapStateToProps = (state, { statusId }) => ({
dropdownPlacement: state.getIn(['dropdown_menu', 'placement']),
openDropdownId: state.getIn(['dropdown_menu', 'openId']),
openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),
items: state.getIn(['history', statusId, 'items']),
loading: state.getIn(['history', statusId, 'loading']),
});
const mapDispatchToProps = (dispatch, { statusId }) => ({
onOpen (id, onItemClick, dropdownPlacement, keyboard) {
dispatch(fetchHistory(statusId));
dispatch(openDropdownMenu(id, dropdownPlacement, keyboard));
},
onClose (id) {
dispatch(closeDropdownMenu(id));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(DropdownMenu);

View File

@ -0,0 +1,70 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, injectIntl } from 'react-intl';
import Icon from 'flavours/glitch/components/icon';
import DropdownMenu from './containers/dropdown_menu_container';
import { connect } from 'react-redux';
import { openModal } from 'flavours/glitch/actions/modal';
import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
import InlineAccount from 'flavours/glitch/components/inline_account';
const mapDispatchToProps = (dispatch, { statusId }) => ({
onItemClick (index) {
dispatch(openModal('COMPARE_HISTORY', { index, statusId }));
},
});
export default @connect(null, mapDispatchToProps)
@injectIntl
class EditedTimestamp extends React.PureComponent {
static propTypes = {
statusId: PropTypes.string.isRequired,
timestamp: PropTypes.string.isRequired,
intl: PropTypes.object.isRequired,
onItemClick: PropTypes.func.isRequired,
};
handleItemClick = (item, i) => {
const { onItemClick } = this.props;
onItemClick(i);
};
renderHeader = items => {
return (
<FormattedMessage id='status.edited_x_times' defaultMessage='Edited {count, plural, one {{count} time} other {{count} times}}' values={{ count: items.size - 1 }} />
);
}
renderItem = (item, index, { onClick, onKeyPress }) => {
const formattedDate = <RelativeTimestamp timestamp={item.get('created_at')} short={false} />;
const formattedName = <InlineAccount accountId={item.get('account')} />;
const label = item.get('original') ? (
<FormattedMessage id='status.history.created' defaultMessage='{name} created {date}' values={{ name: formattedName, date: formattedDate }} />
) : (
<FormattedMessage id='status.history.edited' defaultMessage='{name} edited {date}' values={{ name: formattedName, date: formattedDate }} />
);
return (
<li className='dropdown-menu__item edited-timestamp__history__item' key={item.get('created_at')}>
<button data-index={index} onClick={onClick} onKeyPress={onKeyPress}>{label}</button>
</li>
);
}
render () {
const { timestamp, intl, statusId } = this.props;
return (
<DropdownMenu statusId={statusId} renderItem={this.renderItem} scrollable renderHeader={this.renderHeader} onItemClick={this.handleItemClick}>
<button className='dropdown-menu__text-button'>
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: intl.formatDate(timestamp, { hour12: false, month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) }} /> <Icon id='caret-down' />
</button>
</DropdownMenu>
);
}
}

View File

@ -0,0 +1,34 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import { makeGetAccount } from 'flavours/glitch/selectors';
import Avatar from 'flavours/glitch/components/avatar';
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
const mapStateToProps = (state, { accountId }) => ({
account: getAccount(state, accountId),
});
return mapStateToProps;
};
export default @connect(makeMapStateToProps)
class InlineAccount extends React.PureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
};
render () {
const { account } = this.props;
return (
<span className='inline-account'>
<Avatar size={13} account={account} /> <strong>{account.get('username')}</strong>
</span>
);
}
}

View File

@ -1,10 +1,31 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
export const CircularProgress = ({ size, strokeWidth }) => {
const viewBox = `0 0 ${size} ${size}`;
const radius = (size - strokeWidth) / 2;
return (
<svg width={size} heigh={size} viewBox={viewBox} className='circular-progress' role='progressbar'>
<circle
fill='none'
cx={size / 2}
cy={size / 2}
r={radius}
strokeWidth={`${strokeWidth}px`}
/>
</svg>
);
};
CircularProgress.propTypes = {
size: PropTypes.number.isRequired,
strokeWidth: PropTypes.number.isRequired,
};
const LoadingIndicator = () => (
<div className='loading-indicator'>
<div className='loading-indicator__figure' />
<FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' />
<CircularProgress size={50} strokeWidth={6} />
</div>
);

View File

@ -5,10 +5,15 @@ import PropTypes from 'prop-types';
const messages = defineMessages({
today: { id: 'relative_time.today', defaultMessage: 'today' },
just_now: { id: 'relative_time.just_now', defaultMessage: 'now' },
just_now_full: { id: 'relative_time.full.just_now', defaultMessage: 'just now' },
seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' },
seconds_full: { id: 'relative_time.full.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} ago' },
minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' },
minutes_full: { id: 'relative_time.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} ago' },
hours: { id: 'relative_time.hours', defaultMessage: '{number}h' },
hours_full: { id: 'relative_time.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} ago' },
days: { id: 'relative_time.days', defaultMessage: '{number}d' },
days_full: { id: 'relative_time.full.days', defaultMessage: '{number, plural, one {# day} other {# days}} ago' },
moments_remaining: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' },
seconds_remaining: { id: 'time_remaining.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} left' },
minutes_remaining: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left' },
@ -66,7 +71,7 @@ const getUnitDelay = units => {
}
};
export const timeAgoString = (intl, date, now, year, timeGiven = true) => {
export const timeAgoString = (intl, date, now, year, timeGiven, short) => {
const delta = now - date.getTime();
let relativeTime;
@ -74,16 +79,16 @@ export const timeAgoString = (intl, date, now, year, timeGiven = true) => {
if (delta < DAY && !timeGiven) {
relativeTime = intl.formatMessage(messages.today);
} else if (delta < 10 * SECOND) {
relativeTime = intl.formatMessage(messages.just_now);
relativeTime = intl.formatMessage(short ? messages.just_now : messages.just_now_full);
} else if (delta < 7 * DAY) {
if (delta < MINUTE) {
relativeTime = intl.formatMessage(messages.seconds, { number: Math.floor(delta / SECOND) });
relativeTime = intl.formatMessage(short ? messages.seconds : messages.seconds_full, { number: Math.floor(delta / SECOND) });
} else if (delta < HOUR) {
relativeTime = intl.formatMessage(messages.minutes, { number: Math.floor(delta / MINUTE) });
relativeTime = intl.formatMessage(short ? messages.minutes : messages.minutes_full, { number: Math.floor(delta / MINUTE) });
} else if (delta < DAY) {
relativeTime = intl.formatMessage(messages.hours, { number: Math.floor(delta / HOUR) });
relativeTime = intl.formatMessage(short ? messages.hours : messages.hours_full, { number: Math.floor(delta / HOUR) });
} else {
relativeTime = intl.formatMessage(messages.days, { number: Math.floor(delta / DAY) });
relativeTime = intl.formatMessage(short ? messages.days : messages.days_full, { number: Math.floor(delta / DAY) });
}
} else if (date.getFullYear() === year) {
relativeTime = intl.formatDate(date, shortDateFormatOptions);
@ -124,6 +129,7 @@ class RelativeTimestamp extends React.Component {
timestamp: PropTypes.string.isRequired,
year: PropTypes.number.isRequired,
futureDate: PropTypes.bool,
short: PropTypes.bool,
};
state = {
@ -132,6 +138,7 @@ class RelativeTimestamp extends React.Component {
static defaultProps = {
year: (new Date()).getFullYear(),
short: true,
};
shouldComponentUpdate (nextProps, nextState) {
@ -176,11 +183,11 @@ class RelativeTimestamp extends React.Component {
}
render () {
const { timestamp, intl, year, futureDate } = this.props;
const { timestamp, intl, year, futureDate, short } = this.props;
const timeGiven = timestamp.includes('T');
const date = new Date(timestamp);
const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now, timeGiven) : timeAgoString(intl, date, this.state.now, year, timeGiven);
const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now, timeGiven) : timeAgoString(intl, date, this.state.now, year, timeGiven, short);
return (
<time dateTime={timestamp} title={intl.formatDate(date, dateFormatOptions)}>

View File

@ -13,6 +13,7 @@ import classNames from 'classnames';
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
edit: { id: 'status.edit', defaultMessage: 'Edit' },
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
@ -126,6 +127,10 @@ class StatusActionBar extends ImmutablePureComponent {
this.props.onDelete(this.props.status, this.context.router.history, true);
}
handleEditClick = () => {
this.props.onEdit(this.props.status, this.context.router.history);
}
handlePinClick = () => {
this.props.onPin(this.props.status);
}
@ -225,6 +230,7 @@ class StatusActionBar extends ImmutablePureComponent {
}
if (writtenByMe) {
// menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
} else {

View File

@ -88,6 +88,14 @@ export default class StatusPrepend extends React.PureComponent {
/>
);
}
case 'update':
return (
<FormattedMessage
id='notification.update'
defaultMessage='{name} edited a post'
values={{ name: link }}
/>
);
}
return null;
}
@ -115,6 +123,9 @@ export default class StatusPrepend extends React.PureComponent {
case 'status':
iconId = 'bell';
break;
case 'update':
iconId = 'pencil';
break;
};
return !type ? null : (

View File

@ -14,15 +14,11 @@ const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
onOpen(id, onItemClick, dropdownPlacement, keyboard) {
dispatch(isUserTouching() ? openModal('ACTIONS', {
status,
actions: items.map(
(item, i) => item ? {
...item,
name: `${item.text}-${i}`,
onClick: item.action ? ((e) => { return onItemClick(i, e) }) : null,
} : null
),
actions: items,
onClick: onItemClick,
}) : openDropdownMenu(id, dropdownPlacement, keyboard, scrollKey));
},
onClose(id) {
dispatch(closeModal('ACTIONS'));
dispatch(closeDropdownMenu(id));

View File

@ -17,7 +17,7 @@ import {
pin,
unpin,
} from 'flavours/glitch/actions/interactions';
import { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/statuses';
import { muteStatus, unmuteStatus, deleteStatus, editStatus } from 'flavours/glitch/actions/statuses';
import { initMuteModal } from 'flavours/glitch/actions/mutes';
import { initBlockModal } from 'flavours/glitch/actions/blocks';
import { initReport } from 'flavours/glitch/actions/reports';
@ -169,6 +169,10 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
}
},
onEdit (status, history) {
dispatch(editStatus(status.get('id'), history));
},
onDirect (account, router) {
dispatch(directCompose(account, router));
},

View File

@ -47,6 +47,7 @@ class ComposeForm extends ImmutablePureComponent {
preselectDate: PropTypes.instanceOf(Date),
isSubmitting: PropTypes.bool,
isChangingUpload: PropTypes.bool,
isEditing: PropTypes.bool,
isUploading: PropTypes.bool,
onChange: PropTypes.func,
onSubmit: PropTypes.func,
@ -293,6 +294,7 @@ class ComposeForm extends ImmutablePureComponent {
spoilerText,
suggestions,
spoilersAlwaysOn,
isEditing,
} = this.props;
const countText = this.getFulltextForCharacterCounting();
@ -364,6 +366,7 @@ class ComposeForm extends ImmutablePureComponent {
<Publisher
countText={countText}
disabled={!this.canSubmit()}
isEditing={isEditing}
onSecondarySubmit={handleSecondarySubmit}
onSubmit={handleSubmit}
privacy={privacy}

View File

@ -21,22 +21,25 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
icon: PropTypes.string,
items: PropTypes.arrayOf(PropTypes.shape({
icon: PropTypes.string,
meta: PropTypes.node,
meta: PropTypes.string,
name: PropTypes.string.isRequired,
on: PropTypes.bool,
text: PropTypes.node,
text: PropTypes.string,
})).isRequired,
onModalOpen: PropTypes.func,
onModalClose: PropTypes.func,
title: PropTypes.string,
value: PropTypes.string,
onChange: PropTypes.func,
noModal: PropTypes.bool,
container: PropTypes.func,
renderItemContents: PropTypes.func,
closeOnChange: PropTypes.bool,
};
static defaultProps = {
closeOnChange: true,
};
state = {
needsModalUpdate: false,
open: false,
openedViaKeyboard: undefined,
placement: 'bottom',
@ -44,10 +47,10 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
// Toggles opening and closing the dropdown.
handleToggle = ({ target, type }) => {
const { onModalOpen, noModal } = this.props;
const { onModalOpen } = this.props;
const { open } = this.state;
if (!noModal && isUserTouching()) {
if (isUserTouching()) {
if (this.state.open) {
this.props.onModalClose();
} else {
@ -107,9 +110,25 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
this.setState({ open: false });
}
handleItemClick = (e) => {
const {
items,
onChange,
onModalClose,
closeOnChange,
} = this.props;
const i = Number(e.currentTarget.getAttribute('data-index'));
const { name } = items[i];
e.preventDefault(); // Prevents focus from changing
if (closeOnChange) onModalClose();
onChange(name);
};
// Creates an action modal object.
handleMakeModal = () => {
const component = this;
const {
items,
onChange,
@ -125,6 +144,8 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
// The object.
return {
renderItemContents: this.props.renderItemContents,
onClick: this.handleItemClick,
actions: items.map(
({
name,
@ -133,48 +154,11 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
...rest,
active: value && name === value,
name,
onClick (e) {
e.preventDefault(); // Prevents focus from changing
onModalClose();
onChange(name);
},
onPassiveClick (e) {
e.preventDefault(); // Prevents focus from changing
onChange(name);
component.setState({ needsModalUpdate: true });
},
})
),
};
}
// If our modal is open and our props update, we need to also update
// the modal.
handleUpdate = () => {
const { onModalOpen } = this.props;
const { needsModalUpdate } = this.state;
// Gets our modal object.
const modal = this.handleMakeModal();
// Reopens the modal with the new object.
if (needsModalUpdate && modal && onModalOpen) {
onModalOpen(modal);
}
}
// Updates our modal as necessary.
componentDidUpdate (prevProps) {
const { items } = this.props;
const { needsModalUpdate } = this.state;
if (needsModalUpdate && items.find(
(item, i) => item.on !== prevProps.items[i].on
)) {
this.handleUpdate();
this.setState({ needsModalUpdate: false });
}
}
// Rendering.
render () {
const {
@ -186,6 +170,8 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
onChange,
value,
container,
renderItemContents,
closeOnChange,
} = this.props;
const { open, placement } = this.state;
const computedClass = classNames('composer--options--dropdown', {
@ -226,10 +212,12 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
>
<DropdownMenu
items={items}
renderItemContents={renderItemContents}
onChange={onChange}
onClose={this.handleClose}
value={value}
openedViaKeyboard={this.state.openedViaKeyboard}
closeOnChange={closeOnChange}
/>
</Overlay>
</div>

View File

@ -2,7 +2,6 @@
import PropTypes from 'prop-types';
import React from 'react';
import spring from 'react-motion/lib/spring';
import Toggle from 'react-toggle';
import ImmutablePureComponent from 'react-immutable-pure-component';
import classNames from 'classnames';
@ -28,18 +27,20 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
icon: PropTypes.string,
meta: PropTypes.node,
name: PropTypes.string.isRequired,
on: PropTypes.bool,
text: PropTypes.node,
})),
onChange: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
style: PropTypes.object,
value: PropTypes.string,
renderItemContents: PropTypes.func,
openedViaKeyboard: PropTypes.bool,
closeOnChange: PropTypes.bool,
};
static defaultProps = {
style: {},
closeOnChange: true,
};
state = {
@ -77,16 +78,19 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
document.removeEventListener('touchend', this.handleDocumentClick, withPassive);
}
handleClick = (name, e) => {
handleClick = (e) => {
const i = Number(e.currentTarget.getAttribute('data-index'));
const {
onChange,
onClose,
closeOnChange,
items,
} = this.props;
const { on } = this.props.items.find(item => item.name === name);
const { name } = this.props.items[i];
e.preventDefault(); // Prevents change in focus on click
if ((on === null || typeof on === 'undefined')) {
if (closeOnChange) {
onClose();
}
onChange(name);
@ -101,11 +105,9 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
}
}
handleKeyDown = (name, e) => {
handleKeyDown = (e) => {
const index = Number(e.currentTarget.getAttribute('data-index'));
const { items } = this.props;
const index = items.findIndex(item => {
return (item.name === name);
});
let element = null;
switch(e.key) {
@ -139,7 +141,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
if (element) {
element.focus();
this.handleChange(element.getAttribute('data-index'));
this.handleChange(this.props.items[Number(element.getAttribute('data-index'))].name);
e.preventDefault();
e.stopPropagation();
}
@ -149,44 +151,40 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
this.focusedItem = c;
}
renderItem = (item) => {
const { name, icon, meta, on, text } = item;
renderItem = (item, i) => {
const { name, icon, meta, text } = item;
const active = (name === (this.props.value || this.state.value));
const computedClass = classNames('composer--options--dropdown--content--item', {
active,
lengthy: meta,
'toggled-off': !on && on !== null && typeof on !== 'undefined',
'toggled-on': on,
'with-icon': icon,
});
const computedClass = classNames('composer--options--dropdown--content--item', { active });
let prefix = null;
let contents = this.props.renderItemContents && this.props.renderItemContents(item, i);
if (on !== null && typeof on !== 'undefined') {
prefix = <Toggle checked={on} onChange={this.handleClick.bind(this, name)} />;
} else if (icon) {
prefix = <Icon className='icon' fixedWidth id={icon} />
if (!contents) {
contents = (
<React.Fragment>
{icon && <Icon className='icon' fixedWidth id={icon} />}
<div className='content'>
<strong>{text}</strong>
{meta}
</div>
</React.Fragment>
);
}
return (
<div
className={computedClass}
onClick={this.handleClick.bind(this, name)}
onKeyDown={this.handleKeyDown.bind(this, name)}
onClick={this.handleClick}
onKeyDown={this.handleKeyDown}
role='option'
tabIndex='0'
key={name}
data-index={name}
data-index={i}
ref={active ? this.setFocusRef : null}
>
{prefix}
<div className='content'>
<strong>{text}</strong>
{meta}
</div>
{contents}
</div>
);
}
@ -229,7 +227,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
transform: mounted ? `scale(${scaleX}, ${scaleY})` : null,
}}
>
{!!items && items.map(item => this.renderItem(item))}
{!!items && items.map((item, i) => this.renderItem(item, i))}
</div>
)}
</Motion>

View File

@ -2,8 +2,10 @@
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import { defineMessages, injectIntl } from 'react-intl';
import spring from 'react-motion/lib/spring';
import Toggle from 'react-toggle';
import { connect } from 'react-redux';
// Components.
import IconButton from 'flavours/glitch/components/icon_button';
@ -80,6 +82,36 @@ const messages = defineMessages({
},
});
@connect((state, { name }) => ({ checked: state.getIn(['compose', 'advanced_options', name]) }))
class ToggleOption extends ImmutablePureComponent {
static propTypes = {
name: PropTypes.string.isRequired,
checked: PropTypes.bool,
onChangeAdvancedOption: PropTypes.func.isRequired,
};
handleChange = () => {
this.props.onChangeAdvancedOption(this.props.name);
};
render() {
const { meta, text, checked } = this.props;
return (
<React.Fragment>
<Toggle checked={checked} onChange={this.handleChange} />
<div className='content'>
<strong>{text}</strong>
{meta}
</div>
</React.Fragment>
);
}
}
export default @injectIntl
class ComposerOptions extends ImmutablePureComponent {
@ -141,6 +173,13 @@ class ComposerOptions extends ImmutablePureComponent {
this.fileElement = fileElement;
}
renderToggleItemContents = (item) => {
const { onChangeAdvancedOption } = this.props;
const { name, meta, text } = item;
return <ToggleOption name={name} text={text} meta={meta} onChangeAdvancedOption={onChangeAdvancedOption} />;
};
// Rendering.
render () {
const {
@ -152,7 +191,6 @@ class ComposerOptions extends ImmutablePureComponent {
hasMedia,
allowPoll,
hasPoll,
intl,
onChangeAdvancedOption,
onChangeContentType,
onChangeVisibility,
@ -164,23 +202,24 @@ class ComposerOptions extends ImmutablePureComponent {
resetFileKey,
spoiler,
showContentTypeChoice,
intl: { formatMessage },
} = this.props;
const contentTypeItems = {
plain: {
icon: 'file-text',
name: 'text/plain',
text: <FormattedMessage {...messages.plain} />,
text: formatMessage(messages.plain),
},
html: {
icon: 'code',
name: 'text/html',
text: <FormattedMessage {...messages.html} />,
text: formatMessage(messages.html),
},
markdown: {
icon: 'arrow-circle-down',
name: 'text/markdown',
text: <FormattedMessage {...messages.markdown} />,
text: formatMessage(messages.markdown),
},
};
@ -204,18 +243,18 @@ class ComposerOptions extends ImmutablePureComponent {
{
icon: 'cloud-upload',
name: 'upload',
text: <FormattedMessage {...messages.upload} />,
text: formatMessage(messages.upload),
},
{
icon: 'paint-brush',
name: 'doodle',
text: <FormattedMessage {...messages.doodle} />,
text: formatMessage(messages.doodle),
},
]}
onChange={this.handleClickAttach}
onModalClose={onModalClose}
onModalOpen={onModalOpen}
title={intl.formatMessage(messages.attach)}
title={formatMessage(messages.attach)}
/>
{!!pollLimits && (
<IconButton
@ -229,7 +268,7 @@ class ComposerOptions extends ImmutablePureComponent {
height: null,
lineHeight: null,
}}
title={intl.formatMessage(hasPoll ? messages.remove_poll : messages.add_poll)}
title={formatMessage(hasPoll ? messages.remove_poll : messages.add_poll)}
/>
)}
<hr />
@ -252,7 +291,7 @@ class ComposerOptions extends ImmutablePureComponent {
onChange={onChangeContentType}
onModalClose={onModalClose}
onModalOpen={onModalOpen}
title={intl.formatMessage(messages.content_type)}
title={formatMessage(messages.content_type)}
value={contentType}
/>
)}
@ -262,7 +301,7 @@ class ComposerOptions extends ImmutablePureComponent {
ariaControls='glitch.composer.spoiler.input'
label='CW'
onClick={onToggleSpoiler}
title={intl.formatMessage(messages.spoiler)}
title={formatMessage(messages.spoiler)}
/>
)}
<Dropdown
@ -271,22 +310,22 @@ class ComposerOptions extends ImmutablePureComponent {
icon='ellipsis-h'
items={advancedOptions ? [
{
meta: <FormattedMessage {...messages.local_only_long} />,
meta: formatMessage(messages.local_only_long),
name: 'do_not_federate',
on: advancedOptions.get('do_not_federate'),
text: <FormattedMessage {...messages.local_only_short} />,
text: formatMessage(messages.local_only_short),
},
{
meta: <FormattedMessage {...messages.threaded_mode_long} />,
meta: formatMessage(messages.threaded_mode_long),
name: 'threaded_mode',
on: advancedOptions.get('threaded_mode'),
text: <FormattedMessage {...messages.threaded_mode_short} />,
text: formatMessage(messages.threaded_mode_short),
},
] : null}
onChange={onChangeAdvancedOption}
renderItemContents={this.renderToggleItemContents}
onModalClose={onModalClose}
onModalOpen={onModalOpen}
title={intl.formatMessage(messages.advanced_options_icon_title)}
title={formatMessage(messages.advanced_options_icon_title)}
closeOnChange={false}
/>
</div>
);

View File

@ -1,46 +1,19 @@
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import { defineMessages, injectIntl } from 'react-intl';
import Dropdown from './dropdown';
const messages = defineMessages({
change_privacy: {
defaultMessage: 'Adjust status privacy',
id: 'privacy.change',
},
direct_long: {
defaultMessage: 'Visible for mentioned users only',
id: 'privacy.direct.long',
},
direct_short: {
defaultMessage: 'Direct',
id: 'privacy.direct.short',
},
private_long: {
defaultMessage: 'Visible for followers only',
id: 'privacy.private.long',
},
private_short: {
defaultMessage: 'Followers-only',
id: 'privacy.private.short',
},
public_long: {
defaultMessage: 'Visible for all, shown in public timelines',
id: 'privacy.public.long',
},
public_short: {
defaultMessage: 'Public',
id: 'privacy.public.short',
},
unlisted_long: {
defaultMessage: 'Visible for all, but not in public timelines',
id: 'privacy.unlisted.long',
},
unlisted_short: {
defaultMessage: 'Unlisted',
id: 'privacy.unlisted.short',
},
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
public_long: { id: 'privacy.public.long', defaultMessage: 'Visible for all, shown in public timelines' },
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Visible for all, but not in public timelines' },
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
private_long: { id: 'privacy.private.long', defaultMessage: 'Visible for followers only' },
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible for mentioned users only' },
change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' },
});
export default @injectIntl
@ -53,40 +26,39 @@ class PrivacyDropdown extends React.PureComponent {
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
noDirect: PropTypes.bool,
noModal: PropTypes.bool,
container: PropTypes.func,
intl: PropTypes.object.isRequired,
};
render () {
const { value, onChange, onModalOpen, onModalClose, disabled, noDirect, noModal, container, intl } = this.props;
const { value, onChange, onModalOpen, onModalClose, disabled, noDirect, container, intl: { formatMessage } } = this.props;
// We predefine our privacy items so that we can easily pick the
// dropdown icon later.
const privacyItems = {
direct: {
icon: 'envelope',
meta: <FormattedMessage {...messages.direct_long} />,
meta: formatMessage(messages.direct_long),
name: 'direct',
text: <FormattedMessage {...messages.direct_short} />,
text: formatMessage(messages.direct_short),
},
private: {
icon: 'lock',
meta: <FormattedMessage {...messages.private_long} />,
meta: formatMessage(messages.private_long),
name: 'private',
text: <FormattedMessage {...messages.private_short} />,
text: formatMessage(messages.private_short),
},
public: {
icon: 'globe',
meta: <FormattedMessage {...messages.public_long} />,
meta: formatMessage(messages.public_long),
name: 'public',
text: <FormattedMessage {...messages.public_short} />,
text: formatMessage(messages.public_short),
},
unlisted: {
icon: 'unlock',
meta: <FormattedMessage {...messages.unlisted_long} />,
meta: formatMessage(messages.unlisted_long),
name: 'unlisted',
text: <FormattedMessage {...messages.unlisted_short} />,
text: formatMessage(messages.unlisted_short),
},
};
@ -104,9 +76,8 @@ class PrivacyDropdown extends React.PureComponent {
onChange={onChange}
onModalClose={onModalClose}
onModalOpen={onModalOpen}
title={intl.formatMessage(messages.change_privacy)}
title={formatMessage(messages.change_privacy)}
container={container}
noModal={noModal}
value={value}
/>
);

View File

@ -2,7 +2,7 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import { defineMessages, injectIntl } from 'react-intl';
import { length } from 'stringz';
import ImmutablePureComponent from 'react-immutable-pure-component';
@ -23,6 +23,7 @@ const messages = defineMessages({
defaultMessage: '{publish}!',
id: 'compose_form.publish_loud',
},
saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' },
});
export default @injectIntl
@ -36,6 +37,7 @@ class Publisher extends ImmutablePureComponent {
onSubmit: PropTypes.func,
privacy: PropTypes.oneOf(['direct', 'private', 'unlisted', 'public']),
sideArm: PropTypes.oneOf(['none', 'direct', 'private', 'unlisted', 'public']),
isEditing: PropTypes.bool,
};
handleSubmit = () => {
@ -43,7 +45,7 @@ class Publisher extends ImmutablePureComponent {
};
render () {
const { countText, disabled, intl, onSecondarySubmit, privacy, sideArm } = this.props;
const { countText, disabled, intl, onSecondarySubmit, privacy, sideArm, isEditing } = this.props;
const diff = maxChars - length(countText || '');
const computedClass = classNames('composer--publisher', {
@ -51,63 +53,37 @@ class Publisher extends ImmutablePureComponent {
over: diff < 0,
});
const privacyIcons = { direct: 'envelope', private: 'lock', public: 'globe', unlisted: 'unlock' };
let publishText;
if (isEditing) {
publishText = intl.formatMessage(messages.saveChanges);
} else if (privacy === 'private' || privacy === 'direct') {
const iconId = privacyIcons[privacy];
publishText = (
<span>
<Icon id={iconId} /> {intl.formatMessage(messages.publish)}
</span>
);
} else {
publishText = privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish);
}
return (
<div className={computedClass}>
{sideArm && sideArm !== 'none' ? (
{sideArm && !isEditing && sideArm !== 'none' ? (
<Button
className='side_arm'
disabled={disabled}
onClick={onSecondarySubmit}
style={{ padding: null }}
text={
<span>
<Icon
id={{
public: 'globe',
unlisted: 'unlock',
private: 'lock',
direct: 'envelope',
}[sideArm]}
/>
</span>
}
text={<Icon id={privacyIcons[sideArm]} />}
title={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${sideArm}.short` })}`}
/>
) : null}
<Button
className='primary'
text={function () {
switch (true) {
case !!sideArm && sideArm !== 'none':
case privacy === 'direct':
case privacy === 'private':
return (
<span>
<Icon
id={{
direct: 'envelope',
private: 'lock',
public: 'globe',
unlisted: 'unlock',
}[privacy]}
/>
{' '}
<FormattedMessage {...messages.publish} />
</span>
);
case privacy === 'public':
return (
<span>
<FormattedMessage
{...messages.publishLoud}
values={{ publish: <FormattedMessage {...messages.publish} /> }}
/>
</span>
);
default:
return <span><FormattedMessage {...messages.publish} /></span>;
}
}()}
text={publishText}
title={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${privacy}.short` })}`}
onClick={this.handleSubmit}
disabled={disabled}

View File

@ -51,6 +51,7 @@ function mapStateToProps (state) {
focusDate: state.getIn(['compose', 'focusDate']),
caretPosition: state.getIn(['compose', 'caretPosition']),
isSubmitting: state.getIn(['compose', 'is_submitting']),
isEditing: state.getIn(['compose', 'id']) !== null,
isChangingUpload: state.getIn(['compose', 'is_changing_upload']),
isUploading: state.getIn(['compose', 'is_uploading']),
layout: state.getIn(['local_settings', 'layout']),

View File

@ -1,14 +1,24 @@
import { connect } from 'react-redux';
import { cancelReplyCompose } from 'flavours/glitch/actions/compose';
import { makeGetStatus } from 'flavours/glitch/selectors';
import ReplyIndicator from '../components/reply_indicator';
function makeMapStateToProps (state) {
const inReplyTo = state.getIn(['compose', 'in_reply_to']);
const makeMapStateToProps = () => {
const mapStateToProps = state => {
let statusId = state.getIn(['compose', 'id'], null);
let editing = true;
return {
status: inReplyTo ? state.getIn(['statuses', inReplyTo]) : null,
if (statusId === null) {
statusId = state.getIn(['compose', 'in_reply_to']);
editing = false;
}
return {
status: state.getIn(['statuses', statusId]),
editing,
};
};
return mapStateToProps;
};
const mapDispatchToProps = dispatch => ({

View File

@ -154,6 +154,17 @@ export default class ColumnSettings extends React.PureComponent {
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'status']} onChange={onChange} label={soundStr} />
</div>
</div>
<div role='group' aria-labelledby='notifications-update'>
<span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.update' defaultMessage='Edits:' /></span>
<div className='column-settings__pillbar'>
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'update']} onChange={onChange} label={alertStr} />
{showPushSettings && <PillBarButton prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'update']} onChange={this.onPushChange} label={pushStr} />}
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'update']} onChange={onChange} label={showStr} />
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'update']} onChange={onChange} label={soundStr} />
</div>
</div>
</div>
);
}

View File

@ -171,6 +171,28 @@ export default class Notification extends ImmutablePureComponent {
unread={this.props.unread}
/>
);
case 'update':
return (
<StatusContainer
containerId={notification.get('id')}
hidden={hidden}
id={notification.get('status')}
account={notification.get('account')}
prepend='update'
muted
notification={notification}
onMoveDown={onMoveDown}
onMoveUp={onMoveUp}
onMention={onMention}
getScrollPosition={getScrollPosition}
updateScrollBottom={updateScrollBottom}
cachedMediaWidth={this.props.cachedMediaWidth}
cacheMediaWidth={this.props.cacheMediaWidth}
onUnmount={this.props.onUnmount}
withDismiss
unread={this.props.unread}
/>
);
default:
return null;
}

View File

@ -11,6 +11,7 @@ import classNames from 'classnames';
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
edit: { id: 'status.edit', defaultMessage: 'Edit' },
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
reply: { id: 'status.reply', defaultMessage: 'Reply' },
@ -52,6 +53,7 @@ class ActionBar extends React.PureComponent {
onMuteConversation: PropTypes.func,
onBlock: PropTypes.func,
onDelete: PropTypes.func.isRequired,
onEdit: PropTypes.func.isRequired,
onDirect: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired,
onReport: PropTypes.func,
@ -84,6 +86,10 @@ class ActionBar extends React.PureComponent {
this.props.onDelete(this.props.status, this.context.router.history, true);
}
handleEditClick = () => {
this.props.onEdit(this.props.status, this.context.router.history);
}
handleDirectClick = () => {
this.props.onDirect(this.props.status.get('account'), this.context.router.history);
}
@ -166,6 +172,7 @@ class ActionBar extends React.PureComponent {
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
menu.push(null);
// menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
} else {

View File

@ -7,7 +7,7 @@ import StatusContent from 'flavours/glitch/components/status_content';
import MediaGallery from 'flavours/glitch/components/media_gallery';
import AttachmentList from 'flavours/glitch/components/attachment_list';
import { Link } from 'react-router-dom';
import { injectIntl, FormattedDate, FormattedMessage } from 'react-intl';
import { injectIntl, FormattedDate } from 'react-intl';
import Card from './card';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Video from 'flavours/glitch/features/video';
@ -19,6 +19,7 @@ import PollContainer from 'flavours/glitch/containers/poll_container';
import Icon from 'flavours/glitch/components/icon';
import AnimatedNumber from 'flavours/glitch/components/animated_number';
import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';
import EditedTimestamp from 'flavours/glitch/components/edited_timestamp';
export default @injectIntl
class DetailedStatus extends ImmutablePureComponent {
@ -265,7 +266,7 @@ class DetailedStatus extends ImmutablePureComponent {
edited = (
<React.Fragment>
<React.Fragment> · </React.Fragment>
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: intl.formatDate(status.get('edited_at'), { hour12: false, month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) }} />
<EditedTimestamp statusId={status.get('id')} timestamp={status.get('edited_at')} />
</React.Fragment>
);
}

View File

@ -26,7 +26,7 @@ import {
directCompose,
} from 'flavours/glitch/actions/compose';
import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
import { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/statuses';
import { muteStatus, unmuteStatus, deleteStatus, editStatus } from 'flavours/glitch/actions/statuses';
import { initMuteModal } from 'flavours/glitch/actions/mutes';
import { initBlockModal } from 'flavours/glitch/actions/blocks';
import { initReport } from 'flavours/glitch/actions/reports';
@ -307,6 +307,10 @@ class Status extends ImmutablePureComponent {
}
}
handleEditClick = (status, history) => {
this.props.dispatch(editStatus(status.get('id'), history));
}
handleDirectClick = (account, router) => {
this.props.dispatch(directCompose(account, router));
}
@ -585,6 +589,7 @@ class Status extends ImmutablePureComponent {
onReblog={this.handleReblogClick}
onBookmark={this.handleBookmarkClick}
onDelete={this.handleDeleteClick}
onEdit={this.handleEditClick}
onDirect={this.handleDirectClick}
onMention={this.handleMentionClick}
onMute={this.handleMuteClick}

View File

@ -7,24 +7,22 @@ import Avatar from 'flavours/glitch/components/avatar';
import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
import DisplayName from 'flavours/glitch/components/display_name';
import classNames from 'classnames';
import Icon from 'flavours/glitch/components/icon';
import Link from 'flavours/glitch/components/link';
import Toggle from 'react-toggle';
import IconButton from 'flavours/glitch/components/icon_button';
export default class ActionsModal extends ImmutablePureComponent {
static propTypes = {
status: ImmutablePropTypes.map,
onClick: PropTypes.func,
actions: PropTypes.arrayOf(PropTypes.shape({
active: PropTypes.bool,
href: PropTypes.string,
icon: PropTypes.string,
meta: PropTypes.node,
meta: PropTypes.string,
name: PropTypes.string,
on: PropTypes.bool,
onPassiveClick: PropTypes.func,
text: PropTypes.node,
text: PropTypes.string,
})),
renderItemContents: PropTypes.func,
};
renderAction = (action, i) => {
@ -32,57 +30,26 @@ export default class ActionsModal extends ImmutablePureComponent {
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
}
const {
active,
href,
icon,
meta,
name,
on,
onClick,
onPassiveClick,
text,
} = action;
const { icon = null, text, meta = null, active = false, href = '#' } = action;
let contents = this.props.renderItemContents && this.props.renderItemContents(action, i);
if (!contents) {
contents = (
<React.Fragment>
{icon && <IconButton title={text} icon={icon} role='presentation' tabIndex='-1' inverted />}
<div>
<div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div>
<div>{meta}</div>
</div>
</React.Fragment>
);
}
return (
<li key={name || i}>
<Link
className={classNames('link', { active })}
href={href}
onClick={on !== null && typeof on !== 'undefined' && onPassiveClick || onClick}
role={onClick ? 'button' : null}
>
{function () {
// We render a `<Toggle>` if we were provided an `on`
// property, and otherwise show an `<Icon>` if available.
switch (true) {
case on !== null && typeof on !== 'undefined':
return (
<Toggle
checked={on}
onChange={onPassiveClick || onClick}
/>
);
case !!icon:
return (
<Icon
className='icon'
fixedWidth
id={icon}
/>
);
default:
return null;
}
}()}
{meta ? (
<div>
<strong>{text}</strong>
{meta}
</div>
) : <div>{text}</div>}
</Link>
<li key={`${text}-${i}`}>
<a href={href} target='_blank' rel='noopener noreferrer' onClick={this.props.onClick} data-index={i} className={classNames('link', { active })}>
{contents}
</a>
</li>
);
}

View File

@ -0,0 +1,79 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import { FormattedMessage } from 'react-intl';
import { closeModal } from 'flavours/glitch/actions/modal';
import emojify from 'flavours/glitch/util/emoji';
import escapeTextContentForBrowser from 'escape-html';
import InlineAccount from 'flavours/glitch/components/inline_account';
import IconButton from 'flavours/glitch/components/icon_button';
import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
const mapStateToProps = (state, { statusId }) => ({
versions: state.getIn(['history', statusId, 'items']),
});
const mapDispatchToProps = dispatch => ({
onClose() {
dispatch(closeModal());
},
});
export default @connect(mapStateToProps, mapDispatchToProps)
class CompareHistoryModal extends React.PureComponent {
static propTypes = {
onClose: PropTypes.func.isRequired,
index: PropTypes.number.isRequired,
statusId: PropTypes.string.isRequired,
versions: ImmutablePropTypes.list.isRequired,
};
render () {
const { index, versions, onClose } = this.props;
const currentVersion = versions.get(index);
const emojiMap = currentVersion.get('emojis').reduce((obj, emoji) => {
obj[`:${emoji.get('shortcode')}:`] = emoji.toJS();
return obj;
}, {});
const content = { __html: emojify(currentVersion.get('content'), emojiMap) };
const spoilerContent = { __html: emojify(escapeTextContentForBrowser(currentVersion.get('spoiler_text')), emojiMap) };
const formattedDate = <RelativeTimestamp timestamp={currentVersion.get('created_at')} short={false} />;
const formattedName = <InlineAccount accountId={currentVersion.get('account')} />;
const label = currentVersion.get('original') ? (
<FormattedMessage id='status.history.created' defaultMessage='{name} created {date}' values={{ name: formattedName, date: formattedDate }} />
) : (
<FormattedMessage id='status.history.edited' defaultMessage='{name} edited {date}' values={{ name: formattedName, date: formattedDate }} />
);
return (
<div className='modal-root__modal compare-history-modal'>
<div className='report-modal__target'>
<IconButton className='report-modal__close' icon='times' onClick={onClose} size={20} />
{label}
</div>
<div className='compare-history-modal__container'>
<div className='status__content'>
{currentVersion.get('spoiler_text').length > 0 && (
<React.Fragment>
<div className='translate' dangerouslySetInnerHTML={spoilerContent} />
<hr />
</React.Fragment>
)}
<div className='status__content__text status__content__text--visible translate' dangerouslySetInnerHTML={content} />
</div>
</div>
</div>
);
}
}

View File

@ -24,6 +24,7 @@ import {
ListEditor,
ListAdder,
PinnedAccountsEditor,
CompareHistoryModal,
} from 'flavours/glitch/util/async-components';
const MODAL_COMPONENTS = {
@ -42,9 +43,10 @@ const MODAL_COMPONENTS = {
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
'EMBED': EmbedModal,
'LIST_EDITOR': ListEditor,
'LIST_ADDER':ListAdder,
'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }),
'LIST_ADDER': ListAdder,
'PINNED_ACCOUNTS_EDITOR': PinnedAccountsEditor,
'COMPARE_HISTORY': CompareHistoryModal,
};
export default class ModalRoot extends React.PureComponent {

View File

@ -46,6 +46,7 @@ import {
INIT_MEDIA_EDIT_MODAL,
COMPOSE_CHANGE_MEDIA_DESCRIPTION,
COMPOSE_CHANGE_MEDIA_FOCUS,
COMPOSE_SET_STATUS,
} from 'flavours/glitch/actions/compose';
import { TIMELINE_DELETE } from 'flavours/glitch/actions/timelines';
import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
@ -75,6 +76,7 @@ const initialState = ImmutableMap({
spoiler: false,
spoiler_text: '',
privacy: null,
id: null,
content_type: defaultContentType || 'text/plain',
text: '',
focusDate: null,
@ -160,6 +162,7 @@ function apiStatusToTextHashtags (state, status) {
function clearAll(state) {
return state.withMutations(map => {
map.set('id', null);
map.set('text', '');
if (defaultContentType) map.set('content_type', defaultContentType);
map.set('spoiler', false);
@ -400,6 +403,7 @@ export default function compose(state = initialState, action) {
.set('elefriend', (state.get('elefriend') + 1) % totalElefriends);
case COMPOSE_REPLY:
return state.withMutations(map => {
map.set('id', null);
map.set('in_reply_to', action.status.get('id'));
map.set('text', statusToTextMentions(state, action.status));
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
@ -434,6 +438,7 @@ export default function compose(state = initialState, action) {
map.set('spoiler', false);
map.set('spoiler_text', '');
map.set('privacy', state.get('default_privacy'));
map.set('id', null);
map.set('poll', null);
map.update(
'advanced_options',
@ -565,6 +570,34 @@ export default function compose(state = initialState, action) {
map.set('spoiler_text', '');
}
if (action.status.get('poll')) {
map.set('poll', ImmutableMap({
options: action.status.getIn(['poll', 'options']).map(x => x.get('title')),
multiple: action.status.getIn(['poll', 'multiple']),
expires_in: expiresInFromExpiresAt(action.status.getIn(['poll', 'expires_at'])),
}));
}
});
case COMPOSE_SET_STATUS:
return state.withMutations(map => {
map.set('id', action.status.get('id'));
map.set('text', action.text);
map.set('in_reply_to', action.status.get('in_reply_to_id'));
map.set('privacy', action.status.get('visibility'));
map.set('media_attachments', action.status.get('media_attachments'));
map.set('focusDate', new Date());
map.set('caretPosition', null);
map.set('idempotencyKey', uuid());
map.set('sensitive', action.status.get('sensitive'));
if (action.spoiler_text.length > 0) {
map.set('spoiler', true);
map.set('spoiler_text', action.spoiler_text);
} else {
map.set('spoiler', false);
map.set('spoiler_text', '');
}
if (action.status.get('poll')) {
map.set('poll', ImmutableMap({
options: action.status.getIn(['poll', 'options']).map(x => x.get('title')),

View File

@ -0,0 +1,28 @@
import { HISTORY_FETCH_REQUEST, HISTORY_FETCH_SUCCESS, HISTORY_FETCH_FAIL } from 'flavours/glitch/actions/history';
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
const initialHistory = ImmutableMap({
loading: false,
items: ImmutableList(),
});
const initialState = ImmutableMap();
export default function history(state = initialState, action) {
switch(action.type) {
case HISTORY_FETCH_REQUEST:
return state.update(action.statusId, initialHistory, history => history.withMutations(map => {
map.set('loading', true);
map.set('items', ImmutableList());
}));
case HISTORY_FETCH_SUCCESS:
return state.update(action.statusId, initialHistory, history => history.withMutations(map => {
map.set('loading', false);
map.set('items', fromJS(action.history.map((x, i) => ({ ...x, account: x.account.id, original: i === 0 })).reverse()));
}));
case HISTORY_FETCH_FAIL:
return state.update(action.statusId, initialHistory, history => history.set('loading', false));
default:
return state;
}
}

View File

@ -41,6 +41,7 @@ import markers from './markers';
import account_notes from './account_notes';
import picture_in_picture from './picture_in_picture';
import accounts_map from './accounts_map';
import history from './history';
const reducers = {
announcements,
@ -85,6 +86,7 @@ const reducers = {
markers,
account_notes,
picture_in_picture,
history,
};
export default combineReducers(reducers);

View File

@ -40,6 +40,7 @@ const initialState = ImmutableMap({
mention: false,
poll: false,
status: false,
update: false,
}),
quickFilter: ImmutableMap({
@ -59,6 +60,7 @@ const initialState = ImmutableMap({
mention: true,
poll: true,
status: true,
update: true,
}),
sounds: ImmutableMap({
@ -69,6 +71,7 @@ const initialState = ImmutableMap({
mention: true,
poll: true,
status: true,
update: true,
}),
}),

View File

@ -1203,6 +1203,10 @@ a.sparkline {
}
}
}
@media screen and (max-width: 930px) {
grid-template-columns: minmax(0, 1fr);
}
}
.account-card {
@ -1410,8 +1414,9 @@ a.sparkline {
}
&__button {
box-sizing: border-box;
flex: 0 0 auto;
width: 100px;
width: 200px;
padding: 15px;
padding-right: 0;
@ -1427,4 +1432,38 @@ a.sparkline {
color: $dark-text-color;
}
}
@media screen and (max-width: 800px) {
border: 0;
&__item {
flex-direction: column;
border: 0;
&__button {
width: 100%;
padding: 15px 0;
}
&__description {
padding: 0;
padding-bottom: 15px;
}
}
}
}
.section-skip-link {
float: right;
a {
color: $ui-highlight-color;
text-decoration: none;
&:hover,
&:focus,
&:active {
text-decoration: underline;
}
}
}

View File

@ -500,8 +500,47 @@
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
z-index: 9999;
ul {
list-style: none;
&__text-button {
display: inline;
color: inherit;
background: transparent;
border: 0;
margin: 0;
padding: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
&:focus {
outline: 1px dotted;
}
}
&__container {
&__header {
border-bottom: 1px solid darken($ui-secondary-color, 8%);
padding: 4px 14px;
padding-bottom: 8px;
font-size: 13px;
line-height: 18px;
color: $inverted-text-color;
}
&__list {
list-style: none;
&--scrollable {
max-height: 300px;
overflow-y: scroll;
}
}
&--loading {
display: flex;
align-items: center;
justify-content: center;
padding: 30px 45px;
}
}
}
@ -541,18 +580,29 @@
}
.dropdown-menu__item {
a {
font-size: 13px;
line-height: 18px;
font-size: 13px;
line-height: 18px;
display: block;
color: $inverted-text-color;
a,
button {
font-family: inherit;
font-size: inherit;
line-height: inherit;
display: block;
width: 100%;
padding: 4px 14px;
border: 0;
margin: 0;
box-sizing: border-box;
text-decoration: none;
background: $ui-secondary-color;
color: $inverted-text-color;
color: inherit;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: inherit;
&:focus,
&:hover,
@ -564,6 +614,42 @@
}
}
.dropdown-menu__item--text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 4px 14px;
}
.dropdown-menu__item.edited-timestamp__history__item {
border-bottom: 1px solid darken($ui-secondary-color, 8%);
&:last-child {
border-bottom: 0;
}
&.dropdown-menu__item--text,
a,
button {
padding: 8px 14px;
}
}
.inline-account {
display: inline-flex;
align-items: center;
vertical-align: top;
.account__avatar {
margin-right: 5px;
border-radius: 50%;
}
strong {
font-weight: 600;
}
}
.dropdown--active .dropdown__content {
display: block;
line-height: 18px;
@ -1229,36 +1315,48 @@ button.icon-button.active i.fa-retweet {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
align-items: center;
justify-content: center;
}
span {
display: block;
float: left;
transform: translateX(-50%);
margin: 82px 0 0 50%;
white-space: nowrap;
.circular-progress {
color: lighten($ui-base-color, 26%);
animation: 1.4s linear 0s infinite normal none running simple-rotate;
circle {
stroke: currentColor;
stroke-dasharray: 80px, 200px;
stroke-dashoffset: 0;
animation: circular-progress 1.4s ease-in-out infinite;
}
}
.loading-indicator__figure {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 42px;
height: 42px;
box-sizing: border-box;
background-color: transparent;
border: 0 solid lighten($ui-base-color, 26%);
border-width: 6px;
border-radius: 50%;
@keyframes circular-progress {
0% {
stroke-dasharray: 1px, 200px;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 100px, 200px;
stroke-dashoffset: -15px;
}
100% {
stroke-dasharray: 100px, 200px;
stroke-dashoffset: -125px;
}
}
.no-reduce-motion .loading-indicator span {
animation: loader-label 1.15s infinite cubic-bezier(0.215, 0.610, 0.355, 1.000);
}
@keyframes simple-rotate {
0% {
transform: rotate(0deg);
}
.no-reduce-motion .loading-indicator__figure {
animation: loader-figure 1.15s infinite cubic-bezier(0.215, 0.610, 0.355, 1.000);
100% {
transform: rotate(360deg);
}
}
@keyframes spring-rotate-in {
@ -1305,40 +1403,6 @@ button.icon-button.active i.fa-retweet {
}
}
@keyframes loader-figure {
0% {
width: 0;
height: 0;
background-color: lighten($ui-base-color, 26%);
}
29% {
background-color: lighten($ui-base-color, 26%);
}
30% {
width: 42px;
height: 42px;
background-color: transparent;
border-width: 21px;
opacity: 1;
}
100% {
width: 42px;
height: 42px;
border-width: 0;
opacity: 0;
background-color: transparent;
}
}
@keyframes loader-label {
0% { opacity: 0.25; }
30% { opacity: 1; }
100% { opacity: 0.25; }
}
.spoiler-button {
top: 0;
left: 0;
@ -1508,6 +1572,41 @@ button.icon-button.active i.fa-retweet {
filter: none;
}
.compare-history-modal {
.report-modal__target {
border-bottom: 1px solid $ui-secondary-color;
}
&__container {
padding: 30px;
pointer-events: all;
}
.status__content {
color: $inverted-text-color;
font-size: 19px;
line-height: 24px;
.emojione {
width: 24px;
height: 24px;
margin: -1px 0 0;
}
a {
color: $highlight-text-color;
}
hr {
height: 0.25rem;
padding: 0;
background-color: $ui-secondary-color;
border: 0;
margin: 20px 0;
}
}
}
.loading-bar {
background-color: $ui-highlight-color;
height: 3px;

View File

@ -420,7 +420,8 @@
.report-modal,
.actions-modal,
.mute-modal,
.block-modal {
.block-modal,
.compare-history-modal {
background: lighten($ui-secondary-color, 8%);
color: $inverted-text-color;
border-radius: 8px;

View File

@ -1,7 +1,9 @@
# (REQUIRED) The location of the pack files.
pack:
about: packs/about.js
admin: packs/admin.js
admin:
- packs/admin.js
- packs/public.js
auth: packs/public.js
common:
filename: packs/common.js

View File

@ -173,3 +173,7 @@ export function Directory () {
export function FollowRecommendations () {
return import(/* webpackChunkName: "features/glitch/async/follow_recommendations" */'flavours/glitch/features/follow_recommendations');
}
export function CompareHistoryModal () {
return import(/*webpackChunkName: "flavours/glitch/async/compare_history_modal" */'flavours/glitch/features/ui/components/compare_history_modal');
}

View File

@ -1,7 +1,9 @@
# (REQUIRED) The location of the pack files inside `pack_directory`.
pack:
about: about.js
admin: admin.js
admin:
- admin.js
- public.js
auth: public.js
common:
filename: common.js

View File

@ -70,6 +70,8 @@ export const INIT_MEDIA_EDIT_MODAL = 'INIT_MEDIA_EDIT_MODAL';
export const COMPOSE_CHANGE_MEDIA_DESCRIPTION = 'COMPOSE_CHANGE_MEDIA_DESCRIPTION';
export const COMPOSE_CHANGE_MEDIA_FOCUS = 'COMPOSE_CHANGE_MEDIA_FOCUS';
export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS';
const messages = defineMessages({
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
@ -83,6 +85,15 @@ export const ensureComposeIsVisible = (getState, routerHistory) => {
}
};
export function setComposeToStatus(status, text, spoiler_text) {
return{
type: COMPOSE_SET_STATUS,
status,
text,
spoiler_text,
};
};
export function changeCompose(text) {
return {
type: COMPOSE_CHANGE,
@ -137,8 +148,9 @@ export function directCompose(account, routerHistory) {
export function submitCompose(routerHistory) {
return function (dispatch, getState) {
const status = getState().getIn(['compose', 'text'], '');
const media = getState().getIn(['compose', 'media_attachments']);
const status = getState().getIn(['compose', 'text'], '');
const media = getState().getIn(['compose', 'media_attachments']);
const statusId = getState().getIn(['compose', 'id'], null);
if ((!status || !status.length) && media.size === 0) {
return;
@ -146,15 +158,18 @@ export function submitCompose(routerHistory) {
dispatch(submitComposeRequest());
api(getState).post('/api/v1/statuses', {
status,
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
media_ids: media.map(item => item.get('id')),
sensitive: getState().getIn(['compose', 'sensitive']),
spoiler_text: getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : '',
visibility: getState().getIn(['compose', 'privacy']),
poll: getState().getIn(['compose', 'poll'], null),
}, {
api(getState).request({
url: statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`,
method: statusId === null ? 'post' : 'put',
data: {
status,
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
media_ids: media.map(item => item.get('id')),
sensitive: getState().getIn(['compose', 'sensitive']),
spoiler_text: getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : '',
visibility: getState().getIn(['compose', 'privacy']),
poll: getState().getIn(['compose', 'poll'], null),
},
headers: {
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
},
@ -176,11 +191,11 @@ export function submitCompose(routerHistory) {
}
};
if (response.data.visibility !== 'direct') {
if (statusId === null && response.data.visibility !== 'direct') {
insertIfOnline('home');
}
if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
if (statusId === null && response.data.in_reply_to_id === null && response.data.visibility === 'public') {
insertIfOnline('community');
if (!response.data.local_only) {
insertIfOnline('public');

View File

@ -0,0 +1,37 @@
import api from '../api';
import { importFetchedAccounts } from './importer';
export const HISTORY_FETCH_REQUEST = 'HISTORY_FETCH_REQUEST';
export const HISTORY_FETCH_SUCCESS = 'HISTORY_FETCH_SUCCESS';
export const HISTORY_FETCH_FAIL = 'HISTORY_FETCH_FAIL';
export const fetchHistory = statusId => (dispatch, getState) => {
const loading = getState().getIn(['history', statusId, 'loading']);
if (loading) {
return;
}
dispatch(fetchHistoryRequest(statusId));
api(getState).get(`/api/v1/statuses/${statusId}/history`).then(({ data }) => {
dispatch(importFetchedAccounts(data.map(x => x.account)));
dispatch(fetchHistorySuccess(statusId, data));
}).catch(error => dispatch(fetchHistoryFail(error)));
};
export const fetchHistoryRequest = statusId => ({
type: HISTORY_FETCH_REQUEST,
statusId,
});
export const fetchHistorySuccess = (statusId, history) => ({
type: HISTORY_FETCH_SUCCESS,
statusId,
history,
});
export const fetchHistoryFail = error => ({
type: HISTORY_FETCH_FAIL,
error,
});

View File

@ -34,7 +34,6 @@ export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING';
export const NOTIFICATIONS_MOUNT = 'NOTIFICATIONS_MOUNT';
export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT';
export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ';
export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT';
@ -124,7 +123,17 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
const excludeTypesFromFilter = filter => {
const allTypes = ImmutableList(['follow', 'follow_request', 'favourite', 'reblog', 'mention', 'poll']);
const allTypes = ImmutableList([
'follow',
'follow_request',
'favourite',
'reblog',
'mention',
'poll',
'status',
'update',
]);
return allTypes.filterNot(item => item === filter).toJS();
};

View File

@ -2,7 +2,7 @@ import api from '../api';
import { deleteFromTimelines } from './timelines';
import { importFetchedStatus, importFetchedStatuses, importFetchedAccount } from './importer';
import { ensureComposeIsVisible } from './compose';
import { ensureComposeIsVisible, setComposeToStatus } from './compose';
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
@ -30,6 +30,10 @@ export const STATUS_COLLAPSE = 'STATUS_COLLAPSE';
export const REDRAFT = 'REDRAFT';
export const STATUS_FETCH_SOURCE_REQUEST = 'STATUS_FETCH_SOURCE_REQUEST';
export const STATUS_FETCH_SOURCE_SUCCESS = 'STATUS_FETCH_SOURCE_SUCCESS';
export const STATUS_FETCH_SOURCE_FAIL = 'STATUS_FETCH_SOURCE_FAIL';
export function fetchStatusRequest(id, skipLoading) {
return {
type: STATUS_FETCH_REQUEST,
@ -84,6 +88,37 @@ export function redraft(status, raw_text) {
};
};
export const editStatus = (id, routerHistory) => (dispatch, getState) => {
let status = getState().getIn(['statuses', id]);
if (status.get('poll')) {
status = status.set('poll', getState().getIn(['polls', status.get('poll')]));
}
dispatch(fetchStatusSourceRequest());
api(getState).get(`/api/v1/statuses/${id}/source`).then(response => {
dispatch(fetchStatusSourceSuccess());
ensureComposeIsVisible(getState, routerHistory);
dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text));
}).catch(error => {
dispatch(fetchStatusSourceFail(error));
});
};
export const fetchStatusSourceRequest = () => ({
type: STATUS_FETCH_SOURCE_REQUEST,
});
export const fetchStatusSourceSuccess = () => ({
type: STATUS_FETCH_SOURCE_SUCCESS,
});
export const fetchStatusSourceFail = error => ({
type: STATUS_FETCH_SOURCE_FAIL,
error,
});
export function deleteStatus(id, routerHistory, withRedraft = false) {
return (dispatch, getState) => {
let status = getState().getIn(['statuses', id]);

View File

@ -6,6 +6,8 @@ import Overlay from 'react-overlays/lib/Overlay';
import Motion from '../features/ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import { supportsPassiveEvents } from 'detect-passive-events';
import classNames from 'classnames';
import { CircularProgress } from 'mastodon/components/loading_indicator';
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
let id = 0;
@ -17,13 +19,18 @@ class DropdownMenu extends React.PureComponent {
};
static propTypes = {
items: PropTypes.array.isRequired,
items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired,
loading: PropTypes.bool,
scrollable: PropTypes.bool,
onClose: PropTypes.func.isRequired,
style: PropTypes.object,
placement: PropTypes.string,
arrowOffsetLeft: PropTypes.string,
arrowOffsetTop: PropTypes.string,
openedViaKeyboard: PropTypes.bool,
renderItem: PropTypes.func,
renderHeader: PropTypes.func,
onItemClick: PropTypes.func.isRequired,
};
static defaultProps = {
@ -45,9 +52,11 @@ class DropdownMenu extends React.PureComponent {
document.addEventListener('click', this.handleDocumentClick, false);
document.addEventListener('keydown', this.handleKeyDown, false);
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
if (this.focusedItem && this.props.openedViaKeyboard) {
this.focusedItem.focus({ preventScroll: true });
}
this.setState({ mounted: true });
}
@ -66,7 +75,7 @@ class DropdownMenu extends React.PureComponent {
}
handleKeyDown = e => {
const items = Array.from(this.node.getElementsByTagName('a'));
const items = Array.from(this.node.querySelectorAll('a, button'));
const index = items.indexOf(document.activeElement);
let element = null;
@ -109,21 +118,11 @@ class DropdownMenu extends React.PureComponent {
}
handleClick = e => {
const i = Number(e.currentTarget.getAttribute('data-index'));
const { action, to } = this.props.items[i];
this.props.onClose();
if (typeof action === 'function') {
e.preventDefault();
action(e);
} else if (to) {
e.preventDefault();
this.context.router.history.push(to);
}
const { onItemClick } = this.props;
onItemClick(e);
}
renderItem (option, i) {
renderItem = (option, i) => {
if (option === null) {
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
}
@ -140,9 +139,11 @@ class DropdownMenu extends React.PureComponent {
}
render () {
const { items, style, placement, arrowOffsetLeft, arrowOffsetTop } = this.props;
const { items, style, placement, arrowOffsetLeft, arrowOffsetTop, scrollable, renderHeader, loading } = this.props;
const { mounted } = this.state;
let renderItem = this.props.renderItem || this.renderItem;
return (
<Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
{({ opacity, scaleX, scaleY }) => (
@ -152,9 +153,23 @@ class DropdownMenu extends React.PureComponent {
<div className={`dropdown-menu ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}>
<div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} />
<ul>
{items.map((option, i) => this.renderItem(option, i))}
</ul>
<div className={classNames('dropdown-menu__container', { 'dropdown-menu__container--loading': loading })}>
{loading && (
<CircularProgress size={30} strokeWidth={3.5} />
)}
{!loading && renderHeader && (
<div className='dropdown-menu__container__header'>
{renderHeader(items)}
</div>
)}
{!loading && (
<ul className={classNames('dropdown-menu__container__list', { 'dropdown-menu__container__list--scrollable': scrollable })}>
{items.map((option, i) => renderItem(option, i, { onClick: this.handleClick, onKeyPress: this.handleItemKeyPress }))}
</ul>
)}
</div>
</div>
)}
</Motion>
@ -170,11 +185,14 @@ export default class Dropdown extends React.PureComponent {
};
static propTypes = {
icon: PropTypes.string.isRequired,
items: PropTypes.array.isRequired,
size: PropTypes.number.isRequired,
children: PropTypes.node,
icon: PropTypes.string,
items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired,
loading: PropTypes.bool,
size: PropTypes.number,
title: PropTypes.string,
disabled: PropTypes.bool,
scrollable: PropTypes.bool,
status: ImmutablePropTypes.map,
isUserTouching: PropTypes.func,
onOpen: PropTypes.func.isRequired,
@ -182,6 +200,9 @@ export default class Dropdown extends React.PureComponent {
dropdownPlacement: PropTypes.string,
openDropdownId: PropTypes.number,
openedViaKeyboard: PropTypes.bool,
renderItem: PropTypes.func,
renderHeader: PropTypes.func,
onItemClick: PropTypes.func,
};
static defaultProps = {
@ -237,17 +258,21 @@ export default class Dropdown extends React.PureComponent {
}
handleItemClick = e => {
const { onItemClick } = this.props;
const i = Number(e.currentTarget.getAttribute('data-index'));
const { action, to } = this.props.items[i];
const item = this.props.items[i];
this.handleClose();
if (typeof action === 'function') {
if (typeof onItemClick === 'function') {
e.preventDefault();
action();
} else if (to) {
onItemClick(item, i);
} else if (item && typeof item.action === 'function') {
e.preventDefault();
this.context.router.history.push(to);
item.action();
} else if (item && item.to) {
e.preventDefault();
this.context.router.history.push(item.to);
}
}
@ -265,29 +290,67 @@ export default class Dropdown extends React.PureComponent {
}
}
close = () => {
this.handleClose();
}
render () {
const { icon, items, size, title, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard } = this.props;
const {
icon,
items,
size,
title,
disabled,
loading,
scrollable,
dropdownPlacement,
openDropdownId,
openedViaKeyboard,
children,
renderItem,
renderHeader,
} = this.props;
const open = this.state.id === openDropdownId;
const button = children ? React.cloneElement(React.Children.only(children), {
ref: this.setTargetRef,
onClick: this.handleClick,
onMouseDown: this.handleMouseDown,
onKeyDown: this.handleButtonKeyDown,
onKeyPress: this.handleKeyPress,
}) : (
<IconButton
icon={icon}
title={title}
active={open}
disabled={disabled}
size={size}
ref={this.setTargetRef}
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
onKeyDown={this.handleButtonKeyDown}
onKeyPress={this.handleKeyPress}
/>
);
return (
<div>
<IconButton
icon={icon}
title={title}
active={open}
disabled={disabled}
size={size}
ref={this.setTargetRef}
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
onKeyDown={this.handleButtonKeyDown}
onKeyPress={this.handleKeyPress}
/>
<React.Fragment>
{button}
<Overlay show={open} placement={dropdownPlacement} target={this.findTarget}>
<DropdownMenu items={items} onClose={this.handleClose} openedViaKeyboard={openedViaKeyboard} />
<DropdownMenu
items={items}
loading={loading}
scrollable={scrollable}
onClose={this.handleClose}
openedViaKeyboard={openedViaKeyboard}
renderItem={renderItem}
renderHeader={renderHeader}
onItemClick={this.handleItemClick}
/>
</Overlay>
</div>
</React.Fragment>
);
}

View File

@ -0,0 +1,27 @@
import { connect } from 'react-redux';
import { openDropdownMenu, closeDropdownMenu } from 'mastodon/actions/dropdown_menu';
import { fetchHistory } from 'mastodon/actions/history';
import DropdownMenu from 'mastodon/components/dropdown_menu';
const mapStateToProps = (state, { statusId }) => ({
dropdownPlacement: state.getIn(['dropdown_menu', 'placement']),
openDropdownId: state.getIn(['dropdown_menu', 'openId']),
openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),
items: state.getIn(['history', statusId, 'items']),
loading: state.getIn(['history', statusId, 'loading']),
});
const mapDispatchToProps = (dispatch, { statusId }) => ({
onOpen (id, onItemClick, dropdownPlacement, keyboard) {
dispatch(fetchHistory(statusId));
dispatch(openDropdownMenu(id, dropdownPlacement, keyboard));
},
onClose (id) {
dispatch(closeDropdownMenu(id));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(DropdownMenu);

View File

@ -0,0 +1,70 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, injectIntl } from 'react-intl';
import Icon from 'mastodon/components/icon';
import DropdownMenu from './containers/dropdown_menu_container';
import { connect } from 'react-redux';
import { openModal } from 'mastodon/actions/modal';
import RelativeTimestamp from 'mastodon/components/relative_timestamp';
import InlineAccount from 'mastodon/components/inline_account';
const mapDispatchToProps = (dispatch, { statusId }) => ({
onItemClick (index) {
dispatch(openModal('COMPARE_HISTORY', { index, statusId }));
},
});
export default @connect(null, mapDispatchToProps)
@injectIntl
class EditedTimestamp extends React.PureComponent {
static propTypes = {
statusId: PropTypes.string.isRequired,
timestamp: PropTypes.string.isRequired,
intl: PropTypes.object.isRequired,
onItemClick: PropTypes.func.isRequired,
};
handleItemClick = (item, i) => {
const { onItemClick } = this.props;
onItemClick(i);
};
renderHeader = items => {
return (
<FormattedMessage id='status.edited_x_times' defaultMessage='Edited {count, plural, one {{count} time} other {{count} times}}' values={{ count: items.size - 1 }} />
);
}
renderItem = (item, index, { onClick, onKeyPress }) => {
const formattedDate = <RelativeTimestamp timestamp={item.get('created_at')} short={false} />;
const formattedName = <InlineAccount accountId={item.get('account')} />;
const label = item.get('original') ? (
<FormattedMessage id='status.history.created' defaultMessage='{name} created {date}' values={{ name: formattedName, date: formattedDate }} />
) : (
<FormattedMessage id='status.history.edited' defaultMessage='{name} edited {date}' values={{ name: formattedName, date: formattedDate }} />
);
return (
<li className='dropdown-menu__item edited-timestamp__history__item' key={item.get('created_at')}>
<button data-index={index} onClick={onClick} onKeyPress={onKeyPress}>{label}</button>
</li>
);
}
render () {
const { timestamp, intl, statusId } = this.props;
return (
<DropdownMenu statusId={statusId} renderItem={this.renderItem} scrollable renderHeader={this.renderHeader} onItemClick={this.handleItemClick}>
<button className='dropdown-menu__text-button'>
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: intl.formatDate(timestamp, { hour12: false, month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) }} /> <Icon id='caret-down' />
</button>
</DropdownMenu>
);
}
}

View File

@ -0,0 +1,34 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import { makeGetAccount } from 'mastodon/selectors';
import Avatar from 'mastodon/components/avatar';
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
const mapStateToProps = (state, { accountId }) => ({
account: getAccount(state, accountId),
});
return mapStateToProps;
};
export default @connect(makeMapStateToProps)
class InlineAccount extends React.PureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
};
render () {
const { account } = this.props;
return (
<span className='inline-account'>
<Avatar size={13} account={account} /> <strong>{account.get('username')}</strong>
</span>
);
}
}

View File

@ -1,10 +1,31 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
export const CircularProgress = ({ size, strokeWidth }) => {
const viewBox = `0 0 ${size} ${size}`;
const radius = (size - strokeWidth) / 2;
return (
<svg width={size} heigh={size} viewBox={viewBox} className='circular-progress' role='progressbar'>
<circle
fill='none'
cx={size / 2}
cy={size / 2}
r={radius}
strokeWidth={`${strokeWidth}px`}
/>
</svg>
);
};
CircularProgress.propTypes = {
size: PropTypes.number.isRequired,
strokeWidth: PropTypes.number.isRequired,
};
const LoadingIndicator = () => (
<div className='loading-indicator'>
<div className='loading-indicator__figure' />
<FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' />
<CircularProgress size={50} strokeWidth={6} />
</div>
);

View File

@ -5,10 +5,15 @@ import PropTypes from 'prop-types';
const messages = defineMessages({
today: { id: 'relative_time.today', defaultMessage: 'today' },
just_now: { id: 'relative_time.just_now', defaultMessage: 'now' },
just_now_full: { id: 'relative_time.full.just_now', defaultMessage: 'just now' },
seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' },
seconds_full: { id: 'relative_time.full.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} ago' },
minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' },
minutes_full: { id: 'relative_time.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} ago' },
hours: { id: 'relative_time.hours', defaultMessage: '{number}h' },
hours_full: { id: 'relative_time.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} ago' },
days: { id: 'relative_time.days', defaultMessage: '{number}d' },
days_full: { id: 'relative_time.full.days', defaultMessage: '{number, plural, one {# day} other {# days}} ago' },
moments_remaining: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' },
seconds_remaining: { id: 'time_remaining.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} left' },
minutes_remaining: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left' },
@ -66,7 +71,7 @@ const getUnitDelay = units => {
}
};
export const timeAgoString = (intl, date, now, year, timeGiven = true) => {
export const timeAgoString = (intl, date, now, year, timeGiven, short) => {
const delta = now - date.getTime();
let relativeTime;
@ -74,16 +79,16 @@ export const timeAgoString = (intl, date, now, year, timeGiven = true) => {
if (delta < DAY && !timeGiven) {
relativeTime = intl.formatMessage(messages.today);
} else if (delta < 10 * SECOND) {
relativeTime = intl.formatMessage(messages.just_now);
relativeTime = intl.formatMessage(short ? messages.just_now : messages.just_now_full);
} else if (delta < 7 * DAY) {
if (delta < MINUTE) {
relativeTime = intl.formatMessage(messages.seconds, { number: Math.floor(delta / SECOND) });
relativeTime = intl.formatMessage(short ? messages.seconds : messages.seconds_full, { number: Math.floor(delta / SECOND) });
} else if (delta < HOUR) {
relativeTime = intl.formatMessage(messages.minutes, { number: Math.floor(delta / MINUTE) });
relativeTime = intl.formatMessage(short ? messages.minutes : messages.minutes_full, { number: Math.floor(delta / MINUTE) });
} else if (delta < DAY) {
relativeTime = intl.formatMessage(messages.hours, { number: Math.floor(delta / HOUR) });
relativeTime = intl.formatMessage(short ? messages.hours : messages.hours_full, { number: Math.floor(delta / HOUR) });
} else {
relativeTime = intl.formatMessage(messages.days, { number: Math.floor(delta / DAY) });
relativeTime = intl.formatMessage(short ? messages.days : messages.days_full, { number: Math.floor(delta / DAY) });
}
} else if (date.getFullYear() === year) {
relativeTime = intl.formatDate(date, shortDateFormatOptions);
@ -124,6 +129,7 @@ class RelativeTimestamp extends React.Component {
timestamp: PropTypes.string.isRequired,
year: PropTypes.number.isRequired,
futureDate: PropTypes.bool,
short: PropTypes.bool,
};
state = {
@ -132,6 +138,7 @@ class RelativeTimestamp extends React.Component {
static defaultProps = {
year: (new Date()).getFullYear(),
short: true,
};
shouldComponentUpdate (nextProps, nextState) {
@ -176,11 +183,11 @@ class RelativeTimestamp extends React.Component {
}
render () {
const { timestamp, intl, year, futureDate } = this.props;
const { timestamp, intl, year, futureDate, short } = this.props;
const timeGiven = timestamp.includes('T');
const date = new Date(timestamp);
const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now, timeGiven) : timeAgoString(intl, date, this.state.now, year, timeGiven);
const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now, timeGiven) : timeAgoString(intl, date, this.state.now, year, timeGiven, short);
return (
<time dateTime={timestamp} title={intl.formatDate(date, dateFormatOptions)}>

View File

@ -12,6 +12,7 @@ import classNames from 'classnames';
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
edit: { id: 'status.edit', defaultMessage: 'Edit' },
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
@ -137,6 +138,10 @@ class StatusActionBar extends ImmutablePureComponent {
this.props.onDelete(this.props.status, this.context.router.history, true);
}
handleEditClick = () => {
this.props.onEdit(this.props.status, this.context.router.history);
}
handlePinClick = () => {
this.props.onPin(this.props.status);
}
@ -255,6 +260,7 @@ class StatusActionBar extends ImmutablePureComponent {
}
if (writtenByMe) {
// menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
} else {

View File

@ -24,6 +24,7 @@ import {
hideStatus,
revealStatus,
toggleStatusCollapse,
editStatus,
} from '../actions/statuses';
import {
unmuteAccount,
@ -142,6 +143,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}
},
onEdit (status, history) {
dispatch(editStatus(status.get('id'), history));
},
onDirect (account, router) {
dispatch(directCompose(account, router));
},

View File

@ -29,6 +29,7 @@ const messages = defineMessages({
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' },
publish: { id: 'compose_form.publish', defaultMessage: 'Toot' },
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' },
});
export default @injectIntl
@ -50,6 +51,7 @@ class ComposeForm extends ImmutablePureComponent {
preselectDate: PropTypes.instanceOf(Date),
isSubmitting: PropTypes.bool,
isChangingUpload: PropTypes.bool,
isEditing: PropTypes.bool,
isUploading: PropTypes.bool,
onChange: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
@ -200,7 +202,9 @@ class ComposeForm extends ImmutablePureComponent {
const disabled = this.props.isSubmitting;
let publishText = '';
if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
if (this.props.isEditing) {
publishText = intl.formatMessage(messages.saveChanges);
} else if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
publishText = <span className='compose-form__publish-private'><Icon id='lock' /> {intl.formatMessage(messages.publish)}</span>;
} else {
publishText = this.props.privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish);

View File

@ -21,6 +21,7 @@ const mapStateToProps = state => ({
caretPosition: state.getIn(['compose', 'caretPosition']),
preselectDate: state.getIn(['compose', 'preselectDate']),
isSubmitting: state.getIn(['compose', 'is_submitting']),
isEditing: state.getIn(['compose', 'id']) !== null,
isChangingUpload: state.getIn(['compose', 'is_changing_upload']),
isUploading: state.getIn(['compose', 'is_uploading']),
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),

View File

@ -6,9 +6,20 @@ import ReplyIndicator from '../components/reply_indicator';
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
const mapStateToProps = state => ({
status: getStatus(state, { id: state.getIn(['compose', 'in_reply_to']) }),
});
const mapStateToProps = state => {
let statusId = state.getIn(['compose', 'id'], null);
let editing = true;
if (statusId === null) {
statusId = state.getIn(['compose', 'in_reply_to']);
editing = false;
}
return {
status: getStatus(state, { id: statusId }),
editing,
};
};
return mapStateToProps;
};

View File

@ -153,6 +153,17 @@ export default class ColumnSettings extends React.PureComponent {
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'status']} onChange={onChange} label={soundStr} />
</div>
</div>
<div role='group' aria-labelledby='notifications-update'>
<span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.update' defaultMessage='Edits:' /></span>
<div className='column-settings__row'>
<SettingToggle disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'update']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'update']} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'update']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'update']} onChange={onChange} label={soundStr} />
</div>
</div>
</div>
);
}

View File

@ -19,6 +19,7 @@ const messages = defineMessages({
poll: { id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' },
reblog: { id: 'notification.reblog', defaultMessage: '{name} boosted your status' },
status: { id: 'notification.status', defaultMessage: '{name} just posted' },
update: { id: 'notification.update', defaultMessage: '{name} edited a post' },
});
const notificationForScreenReader = (intl, message, timestamp) => {
@ -273,6 +274,38 @@ class Notification extends ImmutablePureComponent {
);
}
renderUpdate (notification, link) {
const { intl, unread } = this.props;
return (
<HotKeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-update focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.update, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
<div className='notification__message'>
<div className='notification__favourite-icon-wrapper'>
<Icon id='pencil' fixedWidth />
</div>
<span title={notification.get('created_at')}>
<FormattedMessage id='notification.update' defaultMessage='{name} edited a post' values={{ name: link }} />
</span>
</div>
<StatusContainer
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>
);
}
renderPoll (notification, account) {
const { intl, unread } = this.props;
const ownPoll = me === account.get('id');
@ -330,6 +363,8 @@ class Notification extends ImmutablePureComponent {
return this.renderReblog(notification, link);
case 'status':
return this.renderStatus(notification, link);
case 'update':
return this.renderUpdate(notification, link);
case 'poll':
return this.renderPoll(notification, account);
}

View File

@ -11,6 +11,7 @@ import classNames from 'classnames';
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
edit: { id: 'status.edit', defaultMessage: 'Edit' },
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
reply: { id: 'status.reply', defaultMessage: 'Reply' },
@ -59,6 +60,7 @@ class ActionBar extends React.PureComponent {
onFavourite: PropTypes.func.isRequired,
onBookmark: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onEdit: PropTypes.func.isRequired,
onDirect: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired,
onMute: PropTypes.func,
@ -98,6 +100,10 @@ class ActionBar extends React.PureComponent {
this.props.onDelete(this.props.status, this.context.router.history, true);
}
handleEditClick = () => {
this.props.onEdit(this.props.status, this.context.router.history);
}
handleDirectClick = () => {
this.props.onDirect(this.props.status.get('account'), this.context.router.history);
}
@ -209,6 +215,7 @@ class ActionBar extends React.PureComponent {
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
menu.push(null);
// menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
} else {

View File

@ -6,7 +6,7 @@ import DisplayName from '../../../components/display_name';
import StatusContent from '../../../components/status_content';
import MediaGallery from '../../../components/media_gallery';
import { Link } from 'react-router-dom';
import { injectIntl, defineMessages, FormattedDate, FormattedMessage } from 'react-intl';
import { injectIntl, defineMessages, FormattedDate } from 'react-intl';
import Card from './card';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Video from '../../video';
@ -16,6 +16,7 @@ import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import AnimatedNumber from 'mastodon/components/animated_number';
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
import EditedTimestamp from 'mastodon/components/edited_timestamp';
const messages = defineMessages({
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
@ -242,7 +243,7 @@ class DetailedStatus extends ImmutablePureComponent {
edited = (
<React.Fragment>
<React.Fragment> · </React.Fragment>
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: intl.formatDate(status.get('edited_at'), { hour12: false, month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) }} />
<EditedTimestamp statusId={status.get('id')} timestamp={status.get('edited_at')} />
</React.Fragment>
);
}

View File

@ -29,6 +29,7 @@ import {
muteStatus,
unmuteStatus,
deleteStatus,
editStatus,
hideStatus,
revealStatus,
} from '../../actions/statuses';
@ -273,6 +274,10 @@ class Status extends ImmutablePureComponent {
}
}
handleEditClick = (status, history) => {
this.props.dispatch(editStatus(status.get('id'), history));
}
handleDirectClick = (account, router) => {
this.props.dispatch(directCompose(account, router));
}
@ -567,6 +572,7 @@ class Status extends ImmutablePureComponent {
onReblog={this.handleReblogClick}
onBookmark={this.handleBookmarkClick}
onDelete={this.handleDeleteClick}
onEdit={this.handleEditClick}
onDirect={this.handleDirectClick}
onMention={this.handleMentionClick}
onMute={this.handleMuteClick}

View File

@ -0,0 +1,79 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import { FormattedMessage } from 'react-intl';
import { closeModal } from 'mastodon/actions/modal';
import emojify from 'mastodon/features/emoji/emoji';
import escapeTextContentForBrowser from 'escape-html';
import InlineAccount from 'mastodon/components/inline_account';
import IconButton from 'mastodon/components/icon_button';
import RelativeTimestamp from 'mastodon/components/relative_timestamp';
const mapStateToProps = (state, { statusId }) => ({
versions: state.getIn(['history', statusId, 'items']),
});
const mapDispatchToProps = dispatch => ({
onClose() {
dispatch(closeModal());
},
});
export default @connect(mapStateToProps, mapDispatchToProps)
class CompareHistoryModal extends React.PureComponent {
static propTypes = {
onClose: PropTypes.func.isRequired,
index: PropTypes.number.isRequired,
statusId: PropTypes.string.isRequired,
versions: ImmutablePropTypes.list.isRequired,
};
render () {
const { index, versions, onClose } = this.props;
const currentVersion = versions.get(index);
const emojiMap = currentVersion.get('emojis').reduce((obj, emoji) => {
obj[`:${emoji.get('shortcode')}:`] = emoji.toJS();
return obj;
}, {});
const content = { __html: emojify(currentVersion.get('content'), emojiMap) };
const spoilerContent = { __html: emojify(escapeTextContentForBrowser(currentVersion.get('spoiler_text')), emojiMap) };
const formattedDate = <RelativeTimestamp timestamp={currentVersion.get('created_at')} short={false} />;
const formattedName = <InlineAccount accountId={currentVersion.get('account')} />;
const label = currentVersion.get('original') ? (
<FormattedMessage id='status.history.created' defaultMessage='{name} created {date}' values={{ name: formattedName, date: formattedDate }} />
) : (
<FormattedMessage id='status.history.edited' defaultMessage='{name} edited {date}' values={{ name: formattedName, date: formattedDate }} />
);
return (
<div className='modal-root__modal compare-history-modal'>
<div className='report-modal__target'>
<IconButton className='report-modal__close' icon='times' onClick={onClose} size={20} />
{label}
</div>
<div className='compare-history-modal__container'>
<div className='status__content'>
{currentVersion.get('spoiler_text').length > 0 && (
<React.Fragment>
<div className='translate' dangerouslySetInnerHTML={spoilerContent} />
<hr />
</React.Fragment>
)}
<div className='status__content__text status__content__text--visible translate' dangerouslySetInnerHTML={content} />
</div>
</div>
</div>
);
}
}

View File

@ -19,7 +19,8 @@ import {
EmbedModal,
ListEditor,
ListAdder,
} from '../../../features/ui/util/async-components';
CompareHistoryModal,
} from 'mastodon/features/ui/util/async-components';
const MODAL_COMPONENTS = {
'MEDIA': () => Promise.resolve({ default: MediaModal }),
@ -34,7 +35,8 @@ const MODAL_COMPONENTS = {
'EMBED': EmbedModal,
'LIST_EDITOR': ListEditor,
'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }),
'LIST_ADDER':ListAdder,
'LIST_ADDER': ListAdder,
'COMPARE_HISTORY': CompareHistoryModal,
};
export default class ModalRoot extends React.PureComponent {

View File

@ -157,3 +157,7 @@ export function Directory () {
export function FollowRecommendations () {
return import(/* webpackChunkName: "features/follow_recommendations" */'../../follow_recommendations');
}
export function CompareHistoryModal () {
return import(/*webpackChunkName: "modals/compare_history_modal" */'../components/compare_history_modal');
}

View File

@ -47,7 +47,8 @@
"account.unmute": "Unmute @{name}",
"account.unmute_notifications": "Unmute notifications from @{name}",
"account_note.placeholder": "Click to add a note",
"admin.dashboard.retention": "Retention",
"admin.dashboard.daily_retention": "User retention rate by day after sign-up",
"admin.dashboard.monthly_retention": "User retention rate by month after sign-up",
"admin.dashboard.retention.average": "Average",
"admin.dashboard.retention.cohort": "Sign-up month",
"admin.dashboard.retention.cohort_size": "New users",
@ -104,6 +105,7 @@
"compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
"compose_form.publish": "Toot",
"compose_form.publish_loud": "{publish}!",
"compose_form.save_changes": "Save changes",
"compose_form.sensitive.hide": "{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}",
"compose_form.sensitive.marked": "{count, plural, one {Media is marked as sensitive} other {Media is marked as sensitive}}",
"compose_form.sensitive.unmarked": "{count, plural, one {Media is not marked as sensitive} other {Media is not marked as sensitive}}",
@ -312,7 +314,7 @@
"notifications.column_settings.favourite": "Favourites:",
"notifications.column_settings.filter_bar.advanced": "Display all categories",
"notifications.column_settings.filter_bar.category": "Quick filter bar",
"notifications.column_settings.filter_bar.show": "Show",
"notifications.column_settings.filter_bar.show_bar": "Show filter bar",
"notifications.column_settings.follow": "New followers:",
"notifications.column_settings.follow_request": "New follow requests:",
"notifications.column_settings.mention": "Mentions:",
@ -322,7 +324,8 @@
"notifications.column_settings.show": "Show in column",
"notifications.column_settings.sound": "Play sound",
"notifications.column_settings.status": "New toots:",
"notifications.column_settings.unread_markers.category": "Unread notification markers",
"notifications.column_settings.unread_notifications.category": "Unread notifications",
"notifications.column_settings.unread_notifications.highlight": "Highlight unread notifications",
"notifications.filter.all": "All",
"notifications.filter.boosts": "Boosts",
"notifications.filter.favourites": "Favourites",
@ -362,12 +365,20 @@
"regeneration_indicator.label": "Loading…",
"regeneration_indicator.sublabel": "Your home feed is being prepared!",
"relative_time.days": "{number}d",
"relative_time.full.days": "{number, plural, one {# day} other {# days}} ago",
"relative_time.full.hours": "{number, plural, one {# hour} other {# hours}} ago",
"relative_time.full.just_now": "just now",
"relative_time.full.minutes": "{number, plural, one {# minute} other {# minutes}} ago",
"relative_time.full.seconds": "{number, plural, one {# second} other {# seconds}} ago",
"relative_time.hours": "{number}h",
"relative_time.just_now": "now",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"relative_time.today": "today",
"reply_indicator.cancel": "Cancel",
"report.categories.other": "Other",
"report.categories.spam": "Spam",
"report.categories.violation": "Content violates one or more server rules",
"report.forward": "Forward to {target}",
"report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
"report.hint": "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:",
@ -396,9 +407,14 @@
"status.delete": "Delete",
"status.detailed_status": "Detailed conversation view",
"status.direct": "Direct message @{name}",
"status.edit": "Edit",
"status.edited": "Edited {date}",
"status.edited_x_times": "Edited {count, plural, one {{count} time} other {{count} times}}",
"status.embed": "Embed",
"status.favourite": "Favourite",
"status.filtered": "Filtered",
"status.history.created": "{name} created {date}",
"status.history.edited": "{name} edited {date}",
"status.load_more": "Load more",
"status.media_hidden": "Media hidden",
"status.mention": "Mention @{name}",

View File

@ -33,8 +33,8 @@
"account.mute_notifications": "كَتم الإشعارات من @{name}",
"account.muted": "مَكتوم",
"account.never_active": "أبدًا",
"account.posts": "تبويقات",
"account.posts_with_replies": "التَّبويقات والرُدود",
"account.posts": "منشورات",
"account.posts_with_replies": "المنشورات والرُدود",
"account.report": "الإبلاغ عن @{name}",
"account.requested": "في اِنتظر القُبول. اِنقُر لإلغاء طلب المُتابعة",
"account.share": "مُشاركة الملف الشخصي لـ @{name}",
@ -46,19 +46,20 @@
"account.unfollow": "إلغاء المُتابعة",
"account.unmute": "إلغاء الكَتم عن @{name}",
"account.unmute_notifications": "إلغاء كَتم الإشعارات عن @{name}",
"account_note.placeholder": "اِنقُر لإضافة مُلاحظة",
"admin.dashboard.retention": "Retention",
"admin.dashboard.retention.average": "Average",
"admin.dashboard.retention.cohort": "Sign-up month",
"admin.dashboard.retention.cohort_size": "New users",
"account_note.placeholder": "اضغط لإضافة مُلاحظة",
"admin.dashboard.daily_retention": "User retention rate by day after sign-up",
"admin.dashboard.monthly_retention": "User retention rate by month after sign-up",
"admin.dashboard.retention.average": "المعدل",
"admin.dashboard.retention.cohort": "شهر التسجيل",
"admin.dashboard.retention.cohort_size": "المستخدمون الجدد",
"alert.rate_limited.message": "يُرجى إعادة المحاولة بعد {retry_time, time, medium}.",
"alert.rate_limited.title": "المُعَدَّل مَحدود",
"alert.unexpected.message": "لقد طرأ خطأ غير متوقّع.",
"alert.unexpected.title": "المعذرة!",
"announcement.announcement": "إعلان",
"attachments_list.unprocessed": "(unprocessed)",
"attachments_list.unprocessed": "(غير معالَج)",
"autosuggest_hashtag.per_week": "{count} في الأسبوع",
"boost_modal.combo": "يُمكنك الضّغط على {combo} لتخطي هذا في المرة المُقبِلَة",
"boost_modal.combo": "يُمكنك الضّغط على {combo} لتخطي هذا في المرة المُقبلة",
"bundle_column_error.body": "لقد حدث خطأ ما أثناء تحميل هذا العنصر.",
"bundle_column_error.retry": "إعادة المُحاولة",
"bundle_column_error.title": "خطأ في الشبكة",
@ -66,18 +67,18 @@
"bundle_modal_error.message": "لقد حدث خطأ ما أثناء تحميل هذا العنصر.",
"bundle_modal_error.retry": "إعادة المُحاولة",
"column.blocks": "المُستَخدِمون المَحظورون",
"column.bookmarks": "العَلاماتُ المَرجعيَّة",
"column.community": "الخَطُّ الزَّمَنِيُّ المَحَلِّيّ",
"column.direct": "الرَّسَائِلُ المُبَاشِرَة",
"column.directory": "تَصَفُّحُ المَلَفَّاتِ الشَّخصِيَّة",
"column.bookmarks": "الفواصل المرجعية",
"column.community": "الخيط الزمني المحلي",
"column.direct": "الرسائل المباشرة",
"column.directory": "تَصَفُّحُ المَلفات الشخصية",
"column.domain_blocks": "النِّطاقَاتُ المَحظُورَة",
"column.favourites": "المُفَضَّلَة",
"column.follow_requests": "طَلَبَاتُ المُتَابَعَة",
"column.home": "الرَّئِيسَة",
"column.lists": "القَوائِم",
"column.home": "الرئيسية",
"column.lists": "القوائم",
"column.mutes": "المُستَخدِمون المَكتومون",
"column.notifications": "الإشعارَات",
"column.pins": "التَّبويقاتُ المُثَبَّتَة",
"column.pins": "المنشورات المُثَبَّتَة",
"column.public": "الخَطُّ الزَّمَنِيُّ المُوَحَّد",
"column_back_button.label": "العودة",
"column_header.hide_settings": "إخفاء الإعدادات",
@ -86,13 +87,13 @@
"column_header.pin": "تثبيت",
"column_header.show_settings": "إظهار الإعدادات",
"column_header.unpin": "إلغاء التَّثبيت",
"column_subheading.settings": "الإعدَادَات",
"column_subheading.settings": "الإعدادات",
"community.column_settings.local_only": "المحلي فقط",
"community.column_settings.media_only": "الوسائط فقط",
"community.column_settings.remote_only": "عن بُعد فقط",
"compose_form.direct_message_warning": "سوف تُرسَل هذه التَّبويقة فقط للمُستَخدِمين المَذكورين.",
"compose_form.direct_message_warning_learn_more": "تَعَلَّم المَزيد",
"compose_form.hashtag_warning": "لن تُدرَج هذه التبويقة تحت أي وسم بما أنَّها غير مُدرَجة. فقط التبويقات العامة يُمكن البحث عنها بواسطة الوسم.",
"compose_form.hashtag_warning": "لن يُدرَج هذا المنشور تحت أي وسم بما أنَّه غير مُدرَج. فقط المنشورات العامة يُمكن البحث عنها بواسطة الوسم.",
"compose_form.lock_disclaimer": "حسابُك غير {locked}. يُمكن لأي شخص مُتابعتك لرؤية (منشورات المتابعين فقط).",
"compose_form.lock_disclaimer.lock": "مُقفَل",
"compose_form.placeholder": "فِيمَ تُفكِّر؟",
@ -104,10 +105,11 @@
"compose_form.poll.switch_to_single": "تغيِير الاستطلاع للسماح باِخيار واحد فقط",
"compose_form.publish": "تبويق",
"compose_form.publish_loud": "{publish}!",
"compose_form.save_changes": "Save changes",
"compose_form.sensitive.hide": "{count, plural, one {الإشارة إلى الوَسط كمُحتوى حسّاس} two{الإشارة إلى الوسطان كمُحتويان حسّاسان} other {الإشارة إلى الوسائط كمُحتويات حسّاسة}}",
"compose_form.sensitive.marked": "{count, plural, one {تمَّ الإشارة إلى الوسط كمُحتوى حسّاس} two{تمَّ الإشارة إلى الوسطان كمُحتويان حسّاسان} other {تمَّ الإشارة إلى الوسائط كمُحتويات حسّاسة}}",
"compose_form.sensitive.unmarked": "{count, plural, one {لم تَتِمّ الإشارة إلى الوسط كمُحتوى حسّاس} two{لم تَتِمّ الإشارة إلى الوسطان كمُحتويان حسّاسان} other {لم تَتِمّ الإشارة إلى الوسائط كمُحتويات حسّاسة}}",
"compose_form.spoiler.marked": نّ النص مخفي وراء تحذير",
"compose_form.spoiler.marked": زالة تحذير المحتوى",
"compose_form.spoiler.unmarked": "إنَّ النص غير مخفي",
"compose_form.spoiler_placeholder": "اُكتُب تحذيركَ هُنا",
"confirmation_modal.cancel": "إلغاء",
@ -118,8 +120,8 @@
"confirmations.delete.message": "هل أنتَ مُتأكدٌ أنك تُريدُ حَذفَ هذا المنشور؟",
"confirmations.delete_list.confirm": "حذف",
"confirmations.delete_list.message": "هل أنتَ مُتأكدٌ أنكَ تُريدُ حَذفَ هذِهِ القائمةَ بشكلٍ دائم؟",
"confirmations.discard_edit_media.confirm": "Discard",
"confirmations.discard_edit_media.message": "You have unsaved changes to the media description or preview, discard them anyway?",
"confirmations.discard_edit_media.confirm": "تجاهل",
"confirmations.discard_edit_media.message": "لديك تغييرات غير محفوظة لوصف الوسائط أو معاينتها، تجاهلها على أي حال؟",
"confirmations.domain_block.confirm": "حظر اِسم النِّطاق بشكلٍ كامل",
"confirmations.domain_block.message": "متأكد من أنك تود حظر اسم النطاق {domain} بالكامل ؟ في غالب الأحيان يُستَحسَن كتم أو حظر بعض الحسابات بدلا من حظر نطاق بالكامل.\nلن تتمكن مِن رؤية محتوى هذا النطاق لا على خيوطك العمومية و لا في إشعاراتك. سوف يتم كذلك إزالة كافة متابعيك المنتمين إلى هذا النطاق.",
"confirmations.logout.confirm": "خروج",
@ -158,21 +160,21 @@
"emoji_button.symbols": "رموز",
"emoji_button.travel": "الأماكن والسفر",
"empty_column.account_suspended": "حساب معلق",
"empty_column.account_timeline": يس هناك تبويقات!",
"empty_column.account_timeline": ا توجد منشورات هنا!",
"empty_column.account_unavailable": "الملف التعريفي غير متوفر",
"empty_column.blocks": "لم تقم بحظر أي مستخدِم بعد.",
"empty_column.bookmarked_statuses": "ليس لديك أية تبويقات في الفواصل المرجعية بعد. عندما ستقوم بإضافة البعض منها، ستظهر هنا.",
"empty_column.bookmarked_statuses": "ليس لديك أية منشورات في الفواصل المرجعية بعد. عندما ستقوم بإضافة البعض منها، ستظهر هنا.",
"empty_column.community": "الخط العام المحلي فارغ. أكتب شيئا ما للعامة كبداية!",
"empty_column.direct": "لم تتلق أية رسالة خاصة مباشِرة بعد. سوف يتم عرض الرسائل المباشرة هنا إن قمت بإرسال واحدة أو تلقيت البعض منها.",
"empty_column.domain_blocks": "ليس هناك نطاقات مخفية بعد.",
"empty_column.favourited_statuses": "ليس لديك أية تبويقات مفضلة بعد. عندما ستقوم بالإعجاب بواحد، سيظهر هنا.",
"empty_column.favourites": "لم يقم أي أحد بالإعجاب بهذا التبويق بعد. عندما يقوم أحدهم بذلك سوف يظهر هنا.",
"empty_column.favourited_statuses": "ليس لديك أية منشورات مفضلة بعد. عندما ستقوم بالإعجاب بواحدة، ستظهر هنا.",
"empty_column.favourites": "لم يقم أي أحد بالإعجاب بهذا المنشور بعد. عندما يقوم أحدهم بذلك سوف يظهر هنا.",
"empty_column.follow_recommendations": "يبدو أنه لا يمكن إنشاء أي اقتراحات لك. يمكنك البحث عن أشخاص قد تعرفهم أو استكشاف الوسوم الرائجة.",
"empty_column.follow_requests": "ليس عندك أي طلب للمتابعة بعد. سوف تظهر طلباتك هنا إن قمت بتلقي البعض منها.",
"empty_column.hashtag": "ليس هناك بعدُ أي محتوى ذو علاقة بهذا الوسم.",
"empty_column.home": "إنّ الخيط الزمني لصفحتك الرئيسية فارغ. قم بزيارة {public} أو استخدم حقل البحث لكي تكتشف مستخدمين آخرين.",
"empty_column.home.suggestions": "شاهد بعض الاقتراحات",
"empty_column.list": "هذه القائمة فارغة مؤقتا و لكن سوف تمتلئ تدريجيا عندما يبدأ الأعضاء المُنتَمين إليها بنشر تبويقات.",
"empty_column.list": "هذه القائمة فارغة مؤقتا و لكن سوف تمتلئ تدريجيا عندما يبدأ الأعضاء المُنتَمين إليها بنشر منشورات.",
"empty_column.lists": "ليس عندك أية قائمة بعد. سوف تظهر قائمتك هنا إن قمت بإنشاء واحدة.",
"empty_column.mutes": "لم تقم بكتم أي مستخدم بعد.",
"empty_column.notifications": "لم تتلق أي إشعار بعدُ. تفاعل مع المستخدمين الآخرين لإنشاء محادثة.",
@ -185,7 +187,7 @@
"errors.unexpected_crash.report_issue": "الإبلاغ عن خلل",
"follow_recommendations.done": "تم",
"follow_recommendations.heading": "تابع الأشخاص الذين ترغب في رؤية منشوراتهم! إليك بعض الاقتراحات.",
"follow_recommendations.lead": "ستظهر المنشورات من الأشخاص الذين تُتابعتهم بترتيب تسلسلي زمني على صفحتك الرئيسية. لا تخف إذا ارتكبت أي أخطاء، تستطيع إلغاء متابعة أي شخص في أي وقت تريد!",
"follow_recommendations.lead": "ستظهر منشورات الأشخاص الذين تُتابعتهم بترتيب تسلسلي زمني على صفحتك الرئيسية. لا تخف إذا ارتكبت أي أخطاء، تستطيع إلغاء متابعة أي شخص في أي وقت تريد!",
"follow_request.authorize": "ترخيص",
"follow_request.reject": "رفض",
"follow_requests.unlocked_explanation": "على الرغم من أن حسابك غير مقفل، فإن موظفين الـ{domain} ظنوا أنك قد ترغب في مراجعة طلبات المتابعة من هذه الحسابات يدوياً.",
@ -237,7 +239,7 @@
"keyboard_shortcuts.my_profile": "لفتح ملفك التعريفي",
"keyboard_shortcuts.notifications": "لفتح عمود الإشعارات",
"keyboard_shortcuts.open_media": "لفتح الوسائط",
"keyboard_shortcuts.pinned": "لفتح قائمة التبويقات المدبسة",
"keyboard_shortcuts.pinned": "لفتح قائمة المنشورات المثبتة",
"keyboard_shortcuts.profile": "لفتح الملف التعريفي للناشر",
"keyboard_shortcuts.reply": "للردّ",
"keyboard_shortcuts.requests": "لفتح قائمة طلبات المتابعة",
@ -294,7 +296,7 @@
"navigation_bar.logout": "خروج",
"navigation_bar.mutes": "الحسابات المكتومة",
"navigation_bar.personal": "شخصي",
"navigation_bar.pins": "التبويقات المثبتة",
"navigation_bar.pins": "المنشورات المُثَبَّتَة",
"navigation_bar.preferences": "التفضيلات",
"navigation_bar.public_timeline": "الخيط العام الموحد",
"navigation_bar.security": "الأمان",
@ -312,7 +314,7 @@
"notifications.column_settings.favourite": "المُفَضَّلة:",
"notifications.column_settings.filter_bar.advanced": "اعرض كافة الفئات",
"notifications.column_settings.filter_bar.category": "شريط الفلترة السريعة",
"notifications.column_settings.filter_bar.show": "اظهِره",
"notifications.column_settings.filter_bar.show_bar": "إظهار شريط التصفية",
"notifications.column_settings.follow": "متابعُون جُدُد:",
"notifications.column_settings.follow_request": "الطلبات الجديد لِمتابَعتك:",
"notifications.column_settings.mention": "الإشارات:",
@ -321,8 +323,9 @@
"notifications.column_settings.reblog": "الترقيّات:",
"notifications.column_settings.show": "اعرِضها في عمود",
"notifications.column_settings.sound": "أصدر صوتا",
"notifications.column_settings.status": "تبويقات جديدة:",
"notifications.column_settings.unread_markers.category": "علامات إشعار غير مقروءة",
"notifications.column_settings.status": "منشورات جديدة:",
"notifications.column_settings.unread_notifications.category": "إشعارات غير مقروءة",
"notifications.column_settings.unread_notifications.highlight": "علّم الإشعارات غير المقرؤة",
"notifications.filter.all": "الكل",
"notifications.filter.boosts": "الترقيات",
"notifications.filter.favourites": "المفضلة",
@ -346,7 +349,7 @@
"poll.total_votes": "{count, plural, one {# صوت} other {# أصوات}}",
"poll.vote": "صَوّت",
"poll.voted": "لقد صوّتت على هذه الإجابة",
"poll.votes": "{votes, plural, one {# vote} other {# votes}}",
"poll.votes": "{votes, plural, one {# صوت} other {# أصوات}}",
"poll_button.add_poll": "إضافة استطلاع للرأي",
"poll_button.remove_poll": "إزالة استطلاع الرأي",
"privacy.change": "اضبط خصوصية المنشور",
@ -362,12 +365,20 @@
"regeneration_indicator.label": "جارٍ التحميل…",
"regeneration_indicator.sublabel": "جارٍ تجهيز تغذية صفحتك الرئيسية!",
"relative_time.days": "{number}ي",
"relative_time.full.days": "{number, plural, one {# day} other {# days}} ago",
"relative_time.full.hours": "{number, plural, one {# hour} other {# hours}} ago",
"relative_time.full.just_now": "just now",
"relative_time.full.minutes": "{number, plural, one {# minute} other {# minutes}} ago",
"relative_time.full.seconds": "{number, plural, one {# second} other {# seconds}} ago",
"relative_time.hours": "{number}سا",
"relative_time.just_now": "الآن",
"relative_time.minutes": "{number}د",
"relative_time.seconds": "{number}ثا",
"relative_time.today": "اليوم",
"reply_indicator.cancel": "إلغاء",
"report.categories.other": "Other",
"report.categories.spam": "Spam",
"report.categories.violation": "Content violates one or more server rules",
"report.forward": "التحويل إلى {target}",
"report.forward_hint": "هذا الحساب ينتمي إلى خادوم آخَر. هل تودّ إرسال نسخة مجهولة مِن التقرير إلى هنالك أيضًا؟",
"report.hint": "سوف يتم إرسال التقرير إلى المُشرِفين على خادومكم. بإمكانكم الإدلاء بشرح عن سبب الإبلاغ عن الحساب أسفله:",
@ -383,8 +394,8 @@
"search_popout.tips.user": "مستخدِم",
"search_results.accounts": "أشخاص",
"search_results.hashtags": "الوُسوم",
"search_results.statuses": "التبويقات",
"search_results.statuses_fts_disabled": "البحث في التبويقات عن طريق المحتوى ليس مفعل في خادم ماستدون هذا.",
"search_results.statuses": "المنشورات",
"search_results.statuses_fts_disabled": "البحث عن المنشورات عن طريق المحتوى ليس مفعل في خادم ماستدون هذا.",
"search_results.total": "{count, number} {count, plural, zero {} one {نتيجة} two {نتيجتين} few {نتائج} many {نتائج} other {نتائج}}",
"status.admin_account": "افتح الواجهة الإدارية لـ @{name}",
"status.admin_status": "افتح هذا المنشور على واجهة الإشراف",
@ -396,9 +407,14 @@
"status.delete": "احذف",
"status.detailed_status": "تفاصيل المحادثة",
"status.direct": "رسالة خاصة إلى @{name}",
"status.edit": "Edit",
"status.edited": "Edited {date}",
"status.edited_x_times": "Edited {count, plural, one {{count} time} other {{count} times}}",
"status.embed": "إدماج",
"status.favourite": "أضف إلى المفضلة",
"status.filtered": "مُصفّى",
"status.history.created": "{name} created {date}",
"status.history.edited": "{name} edited {date}",
"status.load_more": "حمّل المزيد",
"status.media_hidden": "الصورة مستترة",
"status.mention": "أذكُر @{name}",
@ -443,7 +459,7 @@
"timeline_hint.remote_resource_not_displayed": "{resource} من الخوادم الأخرى لا يتم عرضها.",
"timeline_hint.resources.followers": "المتابِعون",
"timeline_hint.resources.follows": "المتابَعون",
"timeline_hint.resources.statuses": "التبويقات القديمة",
"timeline_hint.resources.statuses": "المنشورات القديمة",
"trends.counter_by_accounts": "{count,plural,zero{} one{{counter} شخص} two{{counter} شخصين} few{{counter} أشخاص } many{{counter} شخص} other{{counter} شخص}}",
"trends.trending_now": "المتداولة الآن",
"ui.beforeunload": "سوف تفقد مسودتك إن تركت ماستدون.",
@ -462,7 +478,7 @@
"upload_form.video_description": "وصف للمعاقين بصريا أو لِذي قِصر السمع",
"upload_modal.analyzing_picture": "جارٍ فحص الصورة…",
"upload_modal.apply": "طبّق",
"upload_modal.applying": "Applying…",
"upload_modal.applying": "جارٍ التطبيق…",
"upload_modal.choose_image": "اختر صورة",
"upload_modal.description_placeholder": "نصٌّ حكيمٌ لهُ سِرٌّ قاطِعٌ وَذُو شَأنٍ عَظيمٍ مكتوبٌ على ثوبٍ أخضرَ ومُغلفٌ بجلدٍ أزرق",
"upload_modal.detect_text": "اكتشف النص مِن الصورة",

View File

@ -47,16 +47,17 @@
"account.unmute": "Unmute @{name}",
"account.unmute_notifications": "Unmute notifications from @{name}",
"account_note.placeholder": "Click to add a note",
"admin.dashboard.retention": "Retention",
"admin.dashboard.daily_retention": "User retention rate by day after sign-up",
"admin.dashboard.monthly_retention": "User retention rate by month after sign-up",
"admin.dashboard.retention.average": "Average",
"admin.dashboard.retention.cohort": "Sign-up month",
"admin.dashboard.retention.cohort_size": "New users",
"alert.rate_limited.message": "Please retry after {retry_time, time, medium}.",
"admin.dashboard.retention.cohort_size": "Usuarios nuevos",
"alert.rate_limited.message": "Volvi tentalo dempués de la hora: {retry_time, time, medium}.",
"alert.rate_limited.title": "Rate limited",
"alert.unexpected.message": "Asocedió un fallu inesperáu.",
"alert.unexpected.title": "¡Meca!",
"announcement.announcement": "Anunciu",
"attachments_list.unprocessed": "(unprocessed)",
"attachments_list.unprocessed": "(ensin procesar)",
"autosuggest_hashtag.per_week": "{count} per selmana",
"boost_modal.combo": "Pues primir {combo} pa saltar esto la próxima vegada",
"bundle_column_error.body": "Asocedió daqué malo mentanto se cargaba esti componente.",
@ -91,7 +92,7 @@
"community.column_settings.media_only": "Namái multimedia",
"community.column_settings.remote_only": "Remote only",
"compose_form.direct_message_warning": "Esti barritu namái va unviase a los usuarios mentaos.",
"compose_form.direct_message_warning_learn_more": "Learn more",
"compose_form.direct_message_warning_learn_more": "Saber más",
"compose_form.hashtag_warning": "This toot won't be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.",
"compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
"compose_form.lock_disclaimer.lock": "locked",
@ -104,6 +105,7 @@
"compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
"compose_form.publish": "Barritar",
"compose_form.publish_loud": "{publish}!",
"compose_form.save_changes": "Save changes",
"compose_form.sensitive.hide": "{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}",
"compose_form.sensitive.marked": "{count, plural, one {Media is marked as sensitive} other {Media is marked as sensitive}}",
"compose_form.sensitive.unmarked": "{count, plural, one {Media is not marked as sensitive} other {Media is not marked as sensitive}}",
@ -163,7 +165,7 @@
"empty_column.blocks": "Entá nun bloquiesti a nunengún usuariu.",
"empty_column.bookmarked_statuses": "Entá nun tienes nengún barritu en Marcadores. Cuando amiestes unu, va amosase equí.",
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
"empty_column.direct": "Entá nun tienes nunengún mensaxe direutu. Cuando unvies o recibas dalgún, va apaecer equí.",
"empty_column.direct": "Entá nun tienes nuengún mensaxe direutu. Cuando unvies o recibas dalgún, va apaecer equí.",
"empty_column.domain_blocks": "Entá nun hai dominios anubríos.",
"empty_column.favourited_statuses": "Entá nun tienes nengún barritu en Favoritos. Cuando amiestes unu, va amosase equí.",
"empty_column.favourites": "No one has favourited this toot yet. When someone does, they will show up here.",
@ -177,7 +179,7 @@
"empty_column.mutes": "Entá nun silenciesti a nunengún usuariu.",
"empty_column.notifications": "Entá nun tienes nunengún avisu. Interactúa con otros p'aniciar la conversación.",
"empty_column.public": "¡Equí nun hai nada! Escribi daqué público o sigui a usuarios d'otros sirvidores pa rellenar esto",
"error.unexpected_crash.explanation": "Pola mor d'un fallu nel códigu o un problema de compatibilidá del restolador, esta páxina nun pudo amosase correutamente.",
"error.unexpected_crash.explanation": "Pola mor d'un fallu nel códigu o un problema de compatibilidá del restolador, esta páxina nun se pudo amosar correutamente.",
"error.unexpected_crash.explanation_addons": "This page could not be displayed correctly. This error is likely caused by a browser add-on or automatic translation tools.",
"error.unexpected_crash.next_steps": "Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.",
"error.unexpected_crash.next_steps_addons": "Try disabling them and refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.",
@ -312,7 +314,7 @@
"notifications.column_settings.favourite": "Favoritos:",
"notifications.column_settings.filter_bar.advanced": "Amosar toles estayes",
"notifications.column_settings.filter_bar.category": "Barra de peñera rápida",
"notifications.column_settings.filter_bar.show": "Amosar",
"notifications.column_settings.filter_bar.show_bar": "Show filter bar",
"notifications.column_settings.follow": "Siguidores nuevos:",
"notifications.column_settings.follow_request": "Solicitúes de siguimientu nueves:",
"notifications.column_settings.mention": "Menciones:",
@ -322,7 +324,8 @@
"notifications.column_settings.show": "Amosar en columna",
"notifications.column_settings.sound": "Reproducir un soníu",
"notifications.column_settings.status": "New toots:",
"notifications.column_settings.unread_markers.category": "Unread notification markers",
"notifications.column_settings.unread_notifications.category": "Unread notifications",
"notifications.column_settings.unread_notifications.highlight": "Highlight unread notifications",
"notifications.filter.all": "Too",
"notifications.filter.boosts": "Boosts",
"notifications.filter.favourites": "Favourites",
@ -336,8 +339,8 @@
"notifications.permission_denied": "Desktop notifications are unavailable due to previously denied browser permissions request",
"notifications.permission_denied_alert": "Desktop notifications can't be enabled, as browser permission has been denied before",
"notifications.permission_required": "Desktop notifications are unavailable because the required permission has not been granted.",
"notifications_permission_banner.enable": "Enable desktop notifications",
"notifications_permission_banner.how_to_control": "To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled.",
"notifications_permission_banner.enable": "Activar los avisos d'escritoriu",
"notifications_permission_banner.how_to_control": "Pa recibir avisos cuando Mastodon nun tea abiertu, activa los avisos del escritoriu. Pues controlar al milímetru qué tipu d'interaiciones xeneren avisos namás que s'activen, pente'l botón {icon} d'arriba.",
"notifications_permission_banner.title": "Never miss a thing",
"picture_in_picture.restore": "Put it back",
"poll.closed": "Acabó",
@ -362,12 +365,20 @@
"regeneration_indicator.label": "Cargando…",
"regeneration_indicator.sublabel": "¡Tamos tresnando'l feed d'Aniciu!",
"relative_time.days": "{number}d",
"relative_time.full.days": "{number, plural, one {# day} other {# days}} ago",
"relative_time.full.hours": "{number, plural, one {# hour} other {# hours}} ago",
"relative_time.full.just_now": "just now",
"relative_time.full.minutes": "{number, plural, one {# minute} other {# minutes}} ago",
"relative_time.full.seconds": "{number, plural, one {# second} other {# seconds}} ago",
"relative_time.hours": "{number}h",
"relative_time.just_now": "agora",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"relative_time.today": "güei",
"reply_indicator.cancel": "Encaboxar",
"report.categories.other": "Other",
"report.categories.spam": "Spam",
"report.categories.violation": "Content violates one or more server rules",
"report.forward": "Forward to {target}",
"report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
"report.hint": "L'informe va unviase a los llendadores del to sirvidor. Embaxo, pues desplicar por qué informes d'esta cuenta:",
@ -396,9 +407,14 @@
"status.delete": "Desaniciar",
"status.detailed_status": "Detailed conversation view",
"status.direct": "Unviar un mensaxe direutu a @{name}",
"status.edit": "Edit",
"status.edited": "Edited {date}",
"status.edited_x_times": "Edited {count, plural, one {{count} time} other {{count} times}}",
"status.embed": "Empotrar",
"status.favourite": "Favourite",
"status.filtered": "Filtered",
"status.history.created": "{name} created {date}",
"status.history.edited": "{name} edited {date}",
"status.load_more": "Cargar más",
"status.media_hidden": "Multimedia anubrida",
"status.mention": "Mentar a @{name}",
@ -408,7 +424,7 @@
"status.open": "Espander esti estáu",
"status.pin": "Fixar nel perfil",
"status.pinned": "Barritu fixáu",
"status.read_more": "Read more",
"status.read_more": "Lleer más",
"status.reblog": "Compartir",
"status.reblog_private": "Compartir cola audiencia orixinal",
"status.reblogged_by": "{name} compartió",
@ -437,7 +453,7 @@
"tabs_bar.search": "Search",
"time_remaining.days": "{number, plural, one {Queda # día} other {Queden # díes}}",
"time_remaining.hours": "{number, plural, one {# hora restante} other {# hores restantes}}",
"time_remaining.minutes": "{number, plural, one {# minutu restante} other {# minutos restantes}}",
"time_remaining.minutes": "{number, plural, one {Queda # minutu} other {Queden # minutos}}",
"time_remaining.moments": "Moments remaining",
"time_remaining.seconds": "{number, plural, one {# segundu restante} other {# segundos restantes}}",
"timeline_hint.remote_resource_not_displayed": "{resource} from other servers are not displayed.",
@ -465,7 +481,7 @@
"upload_modal.applying": "Applying…",
"upload_modal.choose_image": "Choose image",
"upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
"upload_modal.detect_text": "Deteutar el testu de la semeya",
"upload_modal.detect_text": "Detectar el testu de la semeya",
"upload_modal.edit_media": "Edición",
"upload_modal.hint": "Calca o arrastra'l círculu de la previsualización pa escoyer el puntu d'enfoque que va amosase siempres en toles miniatures.",
"upload_modal.preparing_ocr": "Preparing OCR…",
@ -477,8 +493,8 @@
"video.expand": "Espander el videu",
"video.fullscreen": "Pantalla completa",
"video.hide": "Anubrir el videu",
"video.mute": "Silenciar el soníu",
"video.mute": "Desactivar el soníu",
"video.pause": "Posar",
"video.play": "Reproducir",
"video.unmute": "Unmute sound"
"video.unmute": "Activar el soníu"
}

View File

@ -47,7 +47,8 @@
"account.unmute": "Раззаглушаване на @{name}",
"account.unmute_notifications": "Раззаглушаване на известия от @{name}",
"account_note.placeholder": "Click to add a note",
"admin.dashboard.retention": "Retention",
"admin.dashboard.daily_retention": "User retention rate by day after sign-up",
"admin.dashboard.monthly_retention": "User retention rate by month after sign-up",
"admin.dashboard.retention.average": "Average",
"admin.dashboard.retention.cohort": "Sign-up month",
"admin.dashboard.retention.cohort_size": "New users",
@ -104,6 +105,7 @@
"compose_form.poll.switch_to_single": "Промяна на анкетата, за да се позволи един възможен избор",
"compose_form.publish": "Раздумай",
"compose_form.publish_loud": "{publish}!",
"compose_form.save_changes": "Save changes",
"compose_form.sensitive.hide": "{count, plural, one {Маркиране на мултимедията като деликатна} other {Маркиране на мултимедиите като деликатни}}",
"compose_form.sensitive.marked": "{count, plural, one {Мултимедията е маркирана като деликатна} other {Мултимедиите са маркирани като деликатни}}",
"compose_form.sensitive.unmarked": "{count, plural, one {Мултимедията не е маркирана като деликатна} other {Мултимедиите не са маркирани като деликатни}}",
@ -312,7 +314,7 @@
"notifications.column_settings.favourite": "Предпочитани:",
"notifications.column_settings.filter_bar.advanced": "Показване на всички категории",
"notifications.column_settings.filter_bar.category": "Лента за бърз филтър",
"notifications.column_settings.filter_bar.show": "Показване",
"notifications.column_settings.filter_bar.show_bar": "Show filter bar",
"notifications.column_settings.follow": "Нови последователи:",
"notifications.column_settings.follow_request": "Нови заявки за последване:",
"notifications.column_settings.mention": "Споменавания:",
@ -322,7 +324,8 @@
"notifications.column_settings.show": "Покажи в колона",
"notifications.column_settings.sound": "Пускане на звук",
"notifications.column_settings.status": "Нови публикации:",
"notifications.column_settings.unread_markers.category": "отметки за непрочетени известия",
"notifications.column_settings.unread_notifications.category": "Unread notifications",
"notifications.column_settings.unread_notifications.highlight": "Highlight unread notifications",
"notifications.filter.all": "Всичко",
"notifications.filter.boosts": "Споделяния",
"notifications.filter.favourites": "Любими",
@ -362,12 +365,20 @@
"regeneration_indicator.label": "Зареждане…",
"regeneration_indicator.sublabel": "Вашата начална емисия се подготвя!",
"relative_time.days": "{number}д",
"relative_time.full.days": "{number, plural, one {# day} other {# days}} ago",
"relative_time.full.hours": "{number, plural, one {# hour} other {# hours}} ago",
"relative_time.full.just_now": "just now",
"relative_time.full.minutes": "{number, plural, one {# minute} other {# minutes}} ago",
"relative_time.full.seconds": "{number, plural, one {# second} other {# seconds}} ago",
"relative_time.hours": "{number}ч",
"relative_time.just_now": "сега",
"relative_time.minutes": "{number}м",
"relative_time.seconds": "{number}с",
"relative_time.today": "днес",
"reply_indicator.cancel": "Отказ",
"report.categories.other": "Other",
"report.categories.spam": "Spam",
"report.categories.violation": "Content violates one or more server rules",
"report.forward": "Препращане към {target}",
"report.forward_hint": "Акаунтът е от друг сървър. Изпращане на анонимно копие на доклада и там?",
"report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
@ -396,9 +407,14 @@
"status.delete": "Изтриване",
"status.detailed_status": "Подробен изглед на разговор",
"status.direct": "Директно съобщение към @{name}",
"status.edit": "Edit",
"status.edited": "Edited {date}",
"status.edited_x_times": "Edited {count, plural, one {{count} time} other {{count} times}}",
"status.embed": "Вграждане",
"status.favourite": "Предпочитани",
"status.filtered": "Филтрирано",
"status.history.created": "{name} created {date}",
"status.history.edited": "{name} edited {date}",
"status.load_more": "Зареждане на още",
"status.media_hidden": "Мултимедията е скрита",
"status.mention": "Споменаване",

View File

@ -47,7 +47,8 @@
"account.unmute": "@{name} র কার্যকলাপ আবার দেখুন",
"account.unmute_notifications": "@{name} র প্রজ্ঞাপন দেখুন",
"account_note.placeholder": "নোট যোগ করতে ক্লিক করুন",
"admin.dashboard.retention": "Retention",
"admin.dashboard.daily_retention": "User retention rate by day after sign-up",
"admin.dashboard.monthly_retention": "User retention rate by month after sign-up",
"admin.dashboard.retention.average": "Average",
"admin.dashboard.retention.cohort": "Sign-up month",
"admin.dashboard.retention.cohort_size": "New users",
@ -104,6 +105,7 @@
"compose_form.poll.switch_to_single": "একটি একক পছন্দের অনুমতি দেওয়ার জন্য পোল পরিবর্তন করুন",
"compose_form.publish": "টুট",
"compose_form.publish_loud": "{publish}!",
"compose_form.save_changes": "Save changes",
"compose_form.sensitive.hide": "এই ছবি বা ভিডিওটি সংবেদনশীল হিসেবে চিহ্নিত করতে",
"compose_form.sensitive.marked": "এই ছবি বা ভিডিওটি সংবেদনশীল হিসেবে চিহ্নিত করা হয়েছে",
"compose_form.sensitive.unmarked": "এই ছবি বা ভিডিওটি সংবেদনশীল হিসেবে চিহ্নিত করা হয়নি",
@ -312,7 +314,7 @@
"notifications.column_settings.favourite": "পছন্দের:",
"notifications.column_settings.filter_bar.advanced": "সব শ্রেণীগুলো দেখানো",
"notifications.column_settings.filter_bar.category": "সংক্ষিপ্ত ছাঁকনি অংশ",
"notifications.column_settings.filter_bar.show": "দেখানো",
"notifications.column_settings.filter_bar.show_bar": "Show filter bar",
"notifications.column_settings.follow": "নতুন অনুসরণকারীরা:",
"notifications.column_settings.follow_request": "অনুসরণের অনুরোধগুলি:",
"notifications.column_settings.mention": "প্রজ্ঞাপনগুলো:",
@ -322,7 +324,8 @@
"notifications.column_settings.show": "কলামে দেখানো",
"notifications.column_settings.sound": "শব্দ বাজানো",
"notifications.column_settings.status": "New toots:",
"notifications.column_settings.unread_markers.category": "Unread notification markers",
"notifications.column_settings.unread_notifications.category": "Unread notifications",
"notifications.column_settings.unread_notifications.highlight": "Highlight unread notifications",
"notifications.filter.all": "সব",
"notifications.filter.boosts": "সমর্থনগুলো",
"notifications.filter.favourites": "পছন্দের গুলো",
@ -362,12 +365,20 @@
"regeneration_indicator.label": "আসছে…",
"regeneration_indicator.sublabel": "আপনার বাড়ির-সময়রেখা প্রস্তূত করা হচ্ছে!",
"relative_time.days": "{number} দিন",
"relative_time.full.days": "{number, plural, one {# day} other {# days}} ago",
"relative_time.full.hours": "{number, plural, one {# hour} other {# hours}} ago",
"relative_time.full.just_now": "just now",
"relative_time.full.minutes": "{number, plural, one {# minute} other {# minutes}} ago",
"relative_time.full.seconds": "{number, plural, one {# second} other {# seconds}} ago",
"relative_time.hours": "{number} ঘন্টা",
"relative_time.just_now": "এখন",
"relative_time.minutes": "{number}মিঃ",
"relative_time.seconds": "{number} সেকেন্ড",
"relative_time.today": "আজ",
"reply_indicator.cancel": "বাতিল করতে",
"report.categories.other": "Other",
"report.categories.spam": "Spam",
"report.categories.violation": "Content violates one or more server rules",
"report.forward": "এটা আরো পাঠান {target} তে",
"report.forward_hint": "এই নিবন্ধনটি অন্য একটি সার্ভারে। অপ্রকাশিতনামাভাবে রিপোর্টের কপি সেখানেও কি পাঠাতে চান ?",
"report.hint": "রিপোর্টটি আপনার সার্ভারের পরিচালকের কাছে পাঠানো হবে। রিপোর্ট পাঠানোর কারণ নিচে বিস্তারিত লিখতে পারেন:",
@ -396,9 +407,14 @@
"status.delete": "মুছে ফেলতে",
"status.detailed_status": "বিস্তারিত কথোপকথনের হিসেবে দেখতে",
"status.direct": "@{name} কে সরাসরি লেখা পাঠাতে",
"status.edit": "Edit",
"status.edited": "Edited {date}",
"status.edited_x_times": "Edited {count, plural, one {{count} time} other {{count} times}}",
"status.embed": "এমবেড করতে",
"status.favourite": "পছন্দের করতে",
"status.filtered": "ছাঁকনিদিত",
"status.history.created": "{name} created {date}",
"status.history.edited": "{name} edited {date}",
"status.load_more": "আরো দেখুন",
"status.media_hidden": "মিডিয়া লুকানো আছে",
"status.mention": "@{name}কে উল্লেখ করতে",

View File

@ -6,9 +6,9 @@
"account.block": "Berzañ @{name}",
"account.block_domain": "Berzañ pep tra eus {domain}",
"account.blocked": "Stanket",
"account.browse_more_on_origin_server": "Browse more on the original profile",
"account.browse_more_on_origin_server": "Furchal muioc'h war ar profil kentañ",
"account.cancel_follow_request": "Nullañ ar bedadenn heuliañ",
"account.direct": "Kas ur gemennadenn da @{name}",
"account.direct": "Kas ur gemennadenn prevez da @{name}",
"account.disable_notifications": "Paouez d'am c'hemenn pa vez toudet gant @{name}",
"account.domain_blocked": "Domani berzet",
"account.edit_profile": "Aozañ ar profil",
@ -25,7 +25,7 @@
"account.joined": "Amañ abaoe {date}",
"account.last_status": "Oberiantiz zivezhañ",
"account.link_verified_on": "Gwiriet eo bet perc'hennidigezh al liamm d'an deiziad-mañ : {date}",
"account.locked_info": "Prennet eo ar gon-mañ. Dibab a ra ar perc'henn ar re a c'hall heuliañ anezhi pe anezhañ.",
"account.locked_info": "Prennet eo ar gont-mañ. Dibab a ra ar perc'henn ar re a c'hall heuliañ anezhi pe anezhañ.",
"account.media": "Media",
"account.mention": "Menegiñ @{name}",
"account.moved_to": "Dilojet en·he deus {name} da :",
@ -39,7 +39,7 @@
"account.requested": "O c'hortoz an asant. Klikit evit nullañ ar goulenn heuliañ",
"account.share": "Skignañ profil @{name}",
"account.show_reblogs": "Diskouez skignadennoù @{name}",
"account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}",
"account.statuses_counter": "{count, plural, one {{counter} Toud} other {{counter} Toud}}",
"account.unblock": "Diverzañ @{name}",
"account.unblock_domain": "Diverzañ an domani {domain}",
"account.unendorse": "Paouez da lakaat war-wel war ar profil",
@ -47,16 +47,17 @@
"account.unmute": "Diguzhat @{name}",
"account.unmute_notifications": "Diguzhat kemennoù a @{name}",
"account_note.placeholder": "Klikit evit ouzhpenniñ un notenn",
"admin.dashboard.retention": "Retention",
"admin.dashboard.retention.average": "Average",
"admin.dashboard.retention.cohort": "Sign-up month",
"admin.dashboard.retention.cohort_size": "New users",
"admin.dashboard.daily_retention": "User retention rate by day after sign-up",
"admin.dashboard.monthly_retention": "User retention rate by month after sign-up",
"admin.dashboard.retention.average": "Keidenn",
"admin.dashboard.retention.cohort": "Miz an enrolladur",
"admin.dashboard.retention.cohort_size": "Implijerien.erezed nevez",
"alert.rate_limited.message": "Klaskit en-dro a-benn {retry_time, time, medium}.",
"alert.rate_limited.title": "Feur bevennet",
"alert.unexpected.message": "Ur fazi dic'hortozet zo degouezhet.",
"alert.unexpected.title": "Hopala!",
"announcement.announcement": "Kemenn",
"attachments_list.unprocessed": "(unprocessed)",
"attachments_list.unprocessed": "(ket meret)",
"autosuggest_hashtag.per_week": "{count} bep sizhun",
"boost_modal.combo": "Ar wezh kentañ e c'halliot gwaskañ war {combo} evit tremen hebiou",
"bundle_column_error.body": "Degouezhet ez eus bet ur fazi en ur gargañ an elfenn-mañ.",
@ -104,6 +105,7 @@
"compose_form.poll.switch_to_single": "Kemmañ ar sontadeg evit aotren un dibab hepken",
"compose_form.publish": "Toudañ",
"compose_form.publish_loud": "{publish} !",
"compose_form.save_changes": "Save changes",
"compose_form.sensitive.hide": "Merkañ ar media evel kizidik",
"compose_form.sensitive.marked": "Merket eo ar media evel kizidik",
"compose_form.sensitive.unmarked": "N'eo ket merket ar media evel kizidik",
@ -118,8 +120,8 @@
"confirmations.delete.message": "Ha sur oc'h e fell deoc'h dilemel an toud-mañ ?",
"confirmations.delete_list.confirm": "Dilemel",
"confirmations.delete_list.message": "Ha sur eo hoc'h eus c'hoant da zilemel ar roll-mañ da vat ?",
"confirmations.discard_edit_media.confirm": "Discard",
"confirmations.discard_edit_media.message": "You have unsaved changes to the media description or preview, discard them anyway?",
"confirmations.discard_edit_media.confirm": "Nac'hañ",
"confirmations.discard_edit_media.message": "Bez ez eus kemmoù n'int ket enrollet e deskrivadur ar media pe ar rakwel, nullañ anezho evelato?",
"confirmations.domain_block.confirm": "Berzañ an domani a-bezh",
"confirmations.domain_block.message": "Ha sur oc'h e fell deoc'h berzañ an {domain} a-bezh? Peurvuiañ eo trawalc'h berzañ pe mudañ un nebeud implijer·ezed·ien. Ne welot danvez ebet o tont eus an domani-mañ. Dilamet e vo ar c'houmanantoù war an domani-mañ.",
"confirmations.logout.confirm": "Digevreañ",
@ -157,7 +159,7 @@
"emoji_button.search_results": "Disoc'hoù an enklask",
"emoji_button.symbols": "Arouezioù",
"emoji_button.travel": "Lec'hioù ha Beajoù",
"empty_column.account_suspended": "Account suspended",
"empty_column.account_suspended": "Kont ehanet",
"empty_column.account_timeline": "Toud ebet amañ!",
"empty_column.account_unavailable": "Profil dihegerz",
"empty_column.blocks": "N'eus ket bet berzet implijer·ez ganeoc'h c'hoazh.",
@ -167,28 +169,28 @@
"empty_column.domain_blocks": "N'eus domani kuzh ebet c'hoazh.",
"empty_column.favourited_statuses": "N'ho peus toud muiañ-karet ebet c'hoazh. Pa vo lakaet unan ganeoc'h e vo diskouezet amañ.",
"empty_column.favourites": "Den ebet n'eus lakaet an toud-mañ en e reoù muiañ-karet. Pa vo graet gant unan bennak e vo diskouezet amañ.",
"empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.",
"empty_column.follow_recommendations": "Seblant a ra ne vez ket genelet damvenegoù evidoc'h. Gallout a rit implijout un enklask evit klask tud hag a vefe anavezet ganeoc'h pe ergerzhout gerioù-klik diouzh ar c'hiz.",
"empty_column.follow_requests": "N'ho peus goulenn heuliañ ebet c'hoazh. Pa resevot reoù e vo diskouezet amañ.",
"empty_column.hashtag": "N'eus netra er ger-klik-mañ c'hoazh.",
"empty_column.home": "Goullo eo ho red-amzer degemer! Kit da weladenniñ {public} pe implijit ar c'hlask evit kregiñ ganti ha kejañ gant implijer·ien·ezed all.",
"empty_column.home.suggestions": "See some suggestions",
"empty_column.home.suggestions": "Gwellout damvenegoù",
"empty_column.list": "Goullo eo ar roll-mañ evit ar poent. Pa vo toudet gant e izili e vo diskouezet amañ.",
"empty_column.lists": "N'ho peus roll ebet c'hoazh. Pa vo krouet unan ganeoc'h e vo diskouezet amañ.",
"empty_column.mutes": "N'ho peus kuzhet implijer ebet c'hoazh.",
"empty_column.notifications": "N'ho peus kemenn ebet c'hoazh. Grit gant implijer·ezed·ien all evit loc'hañ ar gomz.",
"empty_column.public": "N'eus netra amañ! Skrivit un dra bennak foran pe heuilhit implijer·ien·ezed eus dafariadoù all evit leuniañ",
"error.unexpected_crash.explanation": "Abalamour d'ur beug en hor c'hod pe d'ur gudenn geverlec'hded n'hallomp ket skrammañ ar bajenn-mañ en un doare dereat.",
"error.unexpected_crash.explanation_addons": "This page could not be displayed correctly. This error is likely caused by a browser add-on or automatic translation tools.",
"error.unexpected_crash.explanation_addons": "Ar bajenn-mañ ne c'hell ket bezañ skrammet mat. Ar fazi-se a zo kaoz d'un astenn pe d'un ostilh troidigezh emgefreek war ho merdeer.",
"error.unexpected_crash.next_steps": "Klaskit azbevaat ar bajenn. Ma n'a ket en-dro e c'hallit klask ober gant Mastodon dre ur merdeer disheñvel pe dre an arload genidik.",
"error.unexpected_crash.next_steps_addons": "Try disabling them and refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.",
"error.unexpected_crash.next_steps_addons": "Klaskit azbevaat ar bajenn. Ma n'ez a ket en-dro e c'hallit klask ober gant Mastodon dre ur merdeer disheñvel pe dre an arload genidik.",
"errors.unexpected_crash.copy_stacktrace": "Eilañ ar roudoù diveugañ er golver",
"errors.unexpected_crash.report_issue": "Danevellañ ur fazi",
"follow_recommendations.done": "Graet",
"follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.",
"follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!",
"follow_recommendations.heading": "Heuliit tud e plijfe deoc'h lenn toudoù! Setu un tamm alioù.",
"follow_recommendations.lead": "Toudoù eus tud heuliet ganeoc'h a zeuio war wel en un urzh amzeroniezhel war ho red degemer. N'ho peus ket aon ober fazioù, gallout a rit paouez heuliañ tud ken aes n'eus forzh pegoulz!",
"follow_request.authorize": "Aotren",
"follow_request.reject": "Nac'hañ",
"follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.",
"follow_requests.unlocked_explanation": "Daoust ma n'eo ket ho kont prennet, skipailh {domain} a soñj e fellfe deoc'h gwiriekaat pedadennoù heuliañ deus ar c'hontoù-se diwar-zorn.",
"generic.saved": "Enrollet",
"getting_started.developers": "Diorroerien",
"getting_started.directory": "Roll ar profiloù",
@ -215,43 +217,43 @@
"intervals.full.days": "{number, plural, one {# devezh} other{# a zevezhioù}}",
"intervals.full.hours": "{number, plural, one {# eurvezh} other{# eurvezh}}",
"intervals.full.minutes": "{number, plural, one {# munut} other{# a vunutoù}}",
"keyboard_shortcuts.back": "to navigate back",
"keyboard_shortcuts.blocked": "to open blocked users list",
"keyboard_shortcuts.back": "Distreiñ",
"keyboard_shortcuts.blocked": "Digeriñ roll an implijer.ezed.rien stanket",
"keyboard_shortcuts.boost": "da skignañ",
"keyboard_shortcuts.column": "to focus a status in one of the columns",
"keyboard_shortcuts.compose": "to focus the compose textarea",
"keyboard_shortcuts.column": "Fokus ar bann",
"keyboard_shortcuts.compose": "Fokus an takad testenn",
"keyboard_shortcuts.description": "Deskrivadur",
"keyboard_shortcuts.direct": "to open direct messages column",
"keyboard_shortcuts.down": "to move down in the list",
"keyboard_shortcuts.direct": "Digeriñ bann ar c'hemennadennoù prevez",
"keyboard_shortcuts.down": "Diskennañ er roll",
"keyboard_shortcuts.enter": "evit digeriñ un toud",
"keyboard_shortcuts.favourite": "to favourite",
"keyboard_shortcuts.favourites": "to open favourites list",
"keyboard_shortcuts.federated": "to open federated timeline",
"keyboard_shortcuts.heading": "Keyboard Shortcuts",
"keyboard_shortcuts.home": "to open home timeline",
"keyboard_shortcuts.favourite": "Lakaat an toud evel muiañ-karet",
"keyboard_shortcuts.favourites": "Digeriñ roll an toudoù muiañ-karet",
"keyboard_shortcuts.federated": "Digeriñ ar red-amzer kevreet",
"keyboard_shortcuts.heading": "Berradennoù klavier",
"keyboard_shortcuts.home": "Digeriñ ho red-amzer degemer",
"keyboard_shortcuts.hotkey": "Berradur",
"keyboard_shortcuts.legend": "to display this legend",
"keyboard_shortcuts.local": "to open local timeline",
"keyboard_shortcuts.mention": "to mention author",
"keyboard_shortcuts.muted": "to open muted users list",
"keyboard_shortcuts.my_profile": "to open your profile",
"keyboard_shortcuts.notifications": "to open notifications column",
"keyboard_shortcuts.open_media": "to open media",
"keyboard_shortcuts.pinned": "to open pinned toots list",
"keyboard_shortcuts.profile": "to open author's profile",
"keyboard_shortcuts.legend": "Skrammañ ar skrid-se",
"keyboard_shortcuts.local": "Digeriñ red-amzer lec'hel",
"keyboard_shortcuts.mention": "Menegiñ an aozer.ez",
"keyboard_shortcuts.muted": "Digeriñ roll an implijer.ezed.ien kuzhet",
"keyboard_shortcuts.my_profile": "Digeriñ ho profil",
"keyboard_shortcuts.notifications": "Digeriñ bann kemennoù",
"keyboard_shortcuts.open_media": "Digeriñ ar media",
"keyboard_shortcuts.pinned": "Digeriñ roll an toudoù spilhennet",
"keyboard_shortcuts.profile": "Digeriñ profil an aozer.ez",
"keyboard_shortcuts.reply": "da respont",
"keyboard_shortcuts.requests": "to open follow requests list",
"keyboard_shortcuts.search": "to focus search",
"keyboard_shortcuts.spoilers": "to show/hide CW field",
"keyboard_shortcuts.start": "to open \"get started\" column",
"keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.requests": "Digeriñ roll goulennoù heuliañ",
"keyboard_shortcuts.search": "Fokus barenn klask",
"keyboard_shortcuts.spoilers": "da guzhat/ziguzhat tachenn CW",
"keyboard_shortcuts.start": "Digeriñ bann \"Kregiñ\"",
"keyboard_shortcuts.toggle_hidden": "da guzhat/ziguzhat an desten a-dreñv CW",
"keyboard_shortcuts.toggle_sensitivity": "da guzhat/ziguzhat ur media",
"keyboard_shortcuts.toot": "da gregiñ gant un toud nevez-flamm",
"keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
"keyboard_shortcuts.up": "to move up in the list",
"keyboard_shortcuts.unfocus": "Difokus an dachenn testenn/klask",
"keyboard_shortcuts.up": "Pignat er roll",
"lightbox.close": "Serriñ",
"lightbox.compress": "Compress image view box",
"lightbox.expand": "Expand image view box",
"lightbox.compress": "Bihanaat boest hewel ar skeudenn",
"lightbox.expand": "Ledanaat boest hewel ar skeudenn",
"lightbox.next": "Da-heul",
"lightbox.previous": "A-raok",
"lists.account.add": "Ouzhpennañ d'al listenn",
@ -261,20 +263,20 @@
"lists.edit.submit": "Cheñch an titl",
"lists.new.create": "Ouzhpennañ ul listenn",
"lists.new.title_placeholder": "Titl nevez al listenn",
"lists.replies_policy.followed": "Any followed user",
"lists.replies_policy.followed": "Pep implijer.ez heuliet",
"lists.replies_policy.list": "Izili ar roll",
"lists.replies_policy.none": "Den ebet",
"lists.replies_policy.title": "Diskouez ar respontoù:",
"lists.search": "Klask e-touez tud heuliet ganeoc'h",
"lists.subheading": "Ho listennoù",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
"load_pending": "{count, plural, one {# dra nevez} other {# dra nevez}}",
"loading_indicator.label": "O kargañ...",
"media_gallery.toggle_visible": "Toggle visibility",
"media_gallery.toggle_visible": "{number, plural, one {Kuzhat ar skeudenn} other {Kuzhat ar skeudenn}}",
"missing_indicator.label": "Digavet",
"missing_indicator.sublabel": "This resource could not be found",
"missing_indicator.sublabel": "An danvez-se ne vez ket kavet",
"mute_modal.duration": "Padelezh",
"mute_modal.hide_notifications": "Kuzhat kemenadennoù eus an implijer-se ?",
"mute_modal.indefinite": "Indefinite",
"mute_modal.indefinite": "Amstrizh",
"navigation_bar.apps": "Arloadoù pellgomz",
"navigation_bar.blocks": "Implijer·ezed·ien berzet",
"navigation_bar.bookmarks": "Sinedoù",
@ -287,7 +289,7 @@
"navigation_bar.favourites": "Ar re vuiañ-karet",
"navigation_bar.filters": "Gerioù kuzhet",
"navigation_bar.follow_requests": "Pedadoù heuliañ",
"navigation_bar.follows_and_followers": "Follows and followers",
"navigation_bar.follows_and_followers": "Heuliadennoù ha heulier·ezed·ien",
"navigation_bar.info": "Diwar-benn an dafariad-mañ",
"navigation_bar.keyboard_shortcuts": "Berradurioù",
"navigation_bar.lists": "Listennoù",
@ -298,55 +300,56 @@
"navigation_bar.preferences": "Gwellvezioù",
"navigation_bar.public_timeline": "Red-amzer kevreet",
"navigation_bar.security": "Diogelroez",
"notification.favourite": "{name} favourited your status",
"notification.favourite": "{name} en/he deus lakaet ho toud en e/he muiañ-karet",
"notification.follow": "heuliañ a ra {name} ac'hanoc'h",
"notification.follow_request": "{name} has requested to follow you",
"notification.follow_request": "{name} en/he deus goulennet da heuliañ ac'hanoc'h",
"notification.mention": "{name} en/he deus meneget ac'hanoc'h",
"notification.own_poll": "Echu eo ho sontadeg",
"notification.poll": "Ur sontadeg ho deus mouezhet warnañ a zo echuet",
"notification.reblog": "{name} boosted your status",
"notification.status": "{name} just posted",
"notification.reblog": "{name} skignet ho toud",
"notification.status": "{name} en/he deus toudet",
"notifications.clear": "Skarzhañ ar c'hemennoù",
"notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
"notifications.clear_confirmation": "Ha sur oc'h e fell deoc'h skarzhañ ho kemennoù penn-da-benn?",
"notifications.column_settings.alert": "Kemennoù war ar burev",
"notifications.column_settings.favourite": "Ar re vuiañ-karet:",
"notifications.column_settings.filter_bar.advanced": "Skrammañ an-holl rummadoù",
"notifications.column_settings.filter_bar.category": "Barrenn siloù prim",
"notifications.column_settings.filter_bar.show": "Diskouez",
"notifications.column_settings.filter_bar.show_bar": "Diskouezh barrenn siloù",
"notifications.column_settings.follow": "Heulierien nevez:",
"notifications.column_settings.follow_request": "New follow requests:",
"notifications.column_settings.follow_request": "Pedadoù heuliañ nevez :",
"notifications.column_settings.mention": "Menegoù:",
"notifications.column_settings.poll": "Disoc'hoù ar sontadeg:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.push": "Kemennoù push",
"notifications.column_settings.reblog": "Skignadennoù:",
"notifications.column_settings.show": "Diskouez er bann",
"notifications.column_settings.sound": "Seniñ",
"notifications.column_settings.status": "New toots:",
"notifications.column_settings.unread_markers.category": "Unread notification markers",
"notifications.column_settings.status": "Toudoù nevez:",
"notifications.column_settings.unread_notifications.category": "Kemennoù n'int ket lennet",
"notifications.column_settings.unread_notifications.highlight": "Usskediñ kemennoù nevez",
"notifications.filter.all": "Pep tra",
"notifications.filter.boosts": "Skignadennoù",
"notifications.filter.favourites": "Muiañ-karet",
"notifications.filter.follows": "Heuliañ",
"notifications.filter.mentions": "Menegoù",
"notifications.filter.polls": "Disoc'hoù ar sontadegoù",
"notifications.filter.statuses": "Updates from people you follow",
"notifications.grant_permission": "Grant permission.",
"notifications.filter.statuses": "Hizivadurioù eus tud heuliet ganeoc'h",
"notifications.grant_permission": "Reiñ aotre.",
"notifications.group": "{count} a gemennoù",
"notifications.mark_as_read": "Mark every notification as read",
"notifications.permission_denied": "Desktop notifications are unavailable due to previously denied browser permissions request",
"notifications.permission_denied_alert": "Desktop notifications can't be enabled, as browser permission has been denied before",
"notifications.permission_required": "Desktop notifications are unavailable because the required permission has not been granted.",
"notifications_permission_banner.enable": "Enable desktop notifications",
"notifications_permission_banner.how_to_control": "To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled.",
"notifications_permission_banner.title": "Never miss a thing",
"picture_in_picture.restore": "Put it back",
"notifications.mark_as_read": "Merkañ an holl kemennoù evel bezañ lennet",
"notifications.permission_denied": "Kemennoù war ar burev n'int ket hegerz rak pedadenn aotren ar merdeer a zo bet nullet araok",
"notifications.permission_denied_alert": "Kemennoù wa ar burev na c'hellont ket bezañ lezelet, rak aotre ar merdeer a zo bet nac'het a-raok",
"notifications.permission_required": "Kemennoù war ar burev n'int ket hegerz abalamour d'an aotre rekis n'eo ket bet roet.",
"notifications_permission_banner.enable": "Lezel kemennoù war ar burev",
"notifications_permission_banner.how_to_control": "Evit reseviñ kemennoù pa ne vez ket digoret Mastodon, lezelit kemennoù war ar burev. Gallout a rit kontrollañ peseurt eskemmoù a c'henel kemennoù war ar burev gant ar {icon} nozelenn a-us kentre ma'z int lezelet.",
"notifications_permission_banner.title": "Na vankit netra morse",
"picture_in_picture.restore": "Adlakaat",
"poll.closed": "Serret",
"poll.refresh": "Azbevaat",
"poll.total_people": "{count, plural, one {# person} other {# people}}",
"poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
"poll.total_people": "{count, plural, one {# den} other {# a zen}}",
"poll.total_votes": "{count, plural, one {# votadenn} other {# votadenn}}",
"poll.vote": "Mouezhiañ",
"poll.voted": "Mouezhiet ho peus evit ar respont-mañ",
"poll.votes": "{votes, plural, one {# vote} other {# votes}}",
"poll.votes": "{votes, plural,one {#votadenn} other {# votadenn}}",
"poll_button.add_poll": "Ouzhpennañ ur sontadeg",
"poll_button.remove_poll": "Dilemel ar sontadeg",
"privacy.change": "Kemmañ gwelidigezh ar statud",
@ -362,43 +365,56 @@
"regeneration_indicator.label": "O kargañ…",
"regeneration_indicator.sublabel": "War brientiñ emañ ho red degemer!",
"relative_time.days": "{number}d",
"relative_time.full.days": "{number, plural, one {# day} other {# days}} ago",
"relative_time.full.hours": "{number, plural, one {# hour} other {# hours}} ago",
"relative_time.full.just_now": "just now",
"relative_time.full.minutes": "{number, plural, one {# minute} other {# minutes}} ago",
"relative_time.full.seconds": "{number, plural, one {# second} other {# seconds}} ago",
"relative_time.hours": "{number}e",
"relative_time.just_now": "bremañ",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}eil",
"relative_time.today": "hiziv",
"reply_indicator.cancel": "Nullañ",
"report.categories.other": "Other",
"report.categories.spam": "Spam",
"report.categories.violation": "Content violates one or more server rules",
"report.forward": "Treuzkas da: {target}",
"report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
"report.hint": "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:",
"report.forward_hint": "War ur servijer all emañ ar c'hont-se. Kas dezhañ un adskrid disanv eus an danevell ivez?",
"report.hint": "Ar danevell a vo bet kaset da evezhidi ho servijer. Gallout a rit displegañ perak emaoc'h oc'h aroueziañ ar c'hont a-is:",
"report.placeholder": "Askelennoù ouzhpenn",
"report.submit": "Kinnig",
"report.target": "Report {target}",
"report.target": "O tisklêriañ {target}",
"search.placeholder": "Klask",
"search_popout.search_format": "Advanced search format",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.search_format": "Framm klask araokaet",
"search_popout.tips.full_text": "Testenn simpl a adkas toudoù skrivet ganeoc'h, merket ganeoc'h evel miuañ-karet, toudoù skignet, pe e-lec'h oc'h bet meneget, met ivez anvioù skrammañ, anvioù implijer ha gêrioù-klik hag a glot.",
"search_popout.tips.hashtag": "ger-klik",
"search_popout.tips.status": "statud",
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
"search_popout.tips.status": "toud",
"search_popout.tips.text": "Testenn simpl a adkas anvioù skrammañ, anvioù implijer ha gêrioù-klik hag a glot",
"search_popout.tips.user": "implijer·ez",
"search_results.accounts": "Tud",
"search_results.hashtags": "Gerioù-klik",
"search_results.statuses": "a doudoù",
"search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"status.admin_account": "Open moderation interface for @{name}",
"status.admin_status": "Open this status in the moderation interface",
"search_results.statuses_fts_disabled": "Klask toudoù dre oc'h endalc'h n'eo ket aotreet war ar servijer-mañ.",
"search_results.total": "{count, number} {count, plural, one {disoc'h} other {a zisoc'h}}",
"status.admin_account": "Digeriñ etrefas evezherezh evit @{name}",
"status.admin_status": "Digeriñ an toud e-barzh an etrefas evezherezh",
"status.block": "Berzañ @{name}",
"status.bookmark": "Ouzhpennañ d'ar sinedoù",
"status.cancel_reblog_private": "Unboost",
"status.cannot_reblog": "This post cannot be boosted",
"status.cancel_reblog_private": "Nac'hañ ar skignadenn",
"status.cannot_reblog": "An toud-se ne c'hall ket bezañ skignet",
"status.copy": "Eilañ liamm an toud",
"status.delete": "Dilemel",
"status.detailed_status": "Detailed conversation view",
"status.direct": "Kas ur c'hemennad da @{name}",
"status.detailed_status": "Gwel kaozeadenn munudek",
"status.direct": "Kas ur c'hemennad prevez da @{name}",
"status.edit": "Edit",
"status.edited": "Edited {date}",
"status.edited_x_times": "Edited {count, plural, one {{count} time} other {{count} times}}",
"status.embed": "Enframmañ",
"status.favourite": "Muiañ-karet",
"status.filtered": "Silet",
"status.history.created": "{name} created {date}",
"status.history.edited": "{name} edited {date}",
"status.load_more": "Kargañ muioc'h",
"status.media_hidden": "Media kuzhet",
"status.mention": "Menegiñ @{name}",
@ -410,75 +426,75 @@
"status.pinned": "Toud spilhennet",
"status.read_more": "Lenn muioc'h",
"status.reblog": "Skignañ",
"status.reblog_private": "Boost with original visibility",
"status.reblogged_by": "{name} boosted",
"status.reblogs.empty": "No one has boosted this toot yet. When someone does, they will show up here.",
"status.redraft": "Delete & re-draft",
"status.reblog_private": "Skignañ gant ar weledenn gentañ",
"status.reblogged_by": "{name} en/he deus skignet",
"status.reblogs.empty": "Den ebet n'eus skignet an toud-mañ c'hoazh. Pa vo graet gant unan bennak e vo diskouezet amañ.",
"status.redraft": "Diverkañ ha skrivañ en-dro",
"status.remove_bookmark": "Dilemel ar sined",
"status.reply": "Respont",
"status.replyAll": "Respont d'ar gaozeadenn",
"status.report": "Disklêriañ @{name}",
"status.sensitive_warning": "Sensitive content",
"status.sensitive_warning": "Dalc'had kizidik",
"status.share": "Rannañ",
"status.show_less": "Diskouez nebeutoc'h",
"status.show_less_all": "Show less for all",
"status.show_less_all": "Diskouez nebeutoc'h evit an holl",
"status.show_more": "Diskouez muioc'h",
"status.show_more_all": "Diskouez miuoc'h evit an holl",
"status.show_thread": "Diskouez ar gaozeadenn",
"status.uncached_media_warning": "Dihegerz",
"status.unmute_conversation": "Diguzhat ar gaozeadenn",
"status.unpin": "Dispilhennañ eus ar profil",
"suggestions.dismiss": "Dismiss suggestion",
"suggestions.dismiss": "Dilezel damvenegoù",
"suggestions.header": "Marteze e vefec'h dedenet gant…",
"tabs_bar.federated_timeline": "Kevredet",
"tabs_bar.home": "Degemer",
"tabs_bar.local_timeline": "Lec'hel",
"tabs_bar.notifications": "Kemennoù",
"tabs_bar.search": "Klask",
"time_remaining.days": "{number, plural,one {# devezh} other {# a zevezhioù}} a chom",
"time_remaining.days": "{number, plural,one {# devezh} other {# a zevezh}} a chom",
"time_remaining.hours": "{number, plural, one {# eurvezh} other{# eurvezh}} a chom",
"time_remaining.minutes": "{number, plural, one {# munut} other{# a vunut}} a chom",
"time_remaining.moments": "Moments remaining",
"time_remaining.minutes": "{number, plural, one {# munut} other{# a vunut}} a chom",
"time_remaining.moments": "Pennadoù a-zilerc'h",
"time_remaining.seconds": "{number, plural, one {# eilenn} other{# eilenn}} a chom",
"timeline_hint.remote_resource_not_displayed": "{resource} eus servijerien all n'int ket diskouezet.",
"timeline_hint.remote_resource_not_displayed": "{resource} eus servijerien all n'int ket skrammet.",
"timeline_hint.resources.followers": "Heulier·ezed·ien",
"timeline_hint.resources.follows": "Heuliañ",
"timeline_hint.resources.statuses": "Toudoù koshoc'h",
"trends.counter_by_accounts": "{count, plural, one {{counter} den} other {{counter} a zud}} a zo o komz",
"trends.trending_now": "Luskad ar mare",
"ui.beforeunload": "Kollet e vo ho prell ma kuitit Mastodon.",
"units.short.billion": "{count}B",
"units.short.million": "{count}M",
"units.short.thousand": "{count}K",
"upload_area.title": "Drag & drop to upload",
"units.short.billion": "{count}miliard",
"units.short.million": "{count}milion",
"units.short.thousand": "{count}mil",
"upload_area.title": "Tennañ ha leuskel evit pellgargañ",
"upload_button.label": "Ouzhpennañ ur media",
"upload_error.limit": "File upload limit exceeded.",
"upload_error.poll": "File upload not allowed with polls.",
"upload_error.limit": "Bevenn evit pellgargañ restroù a zo distremenet.",
"upload_error.poll": "Pellgargañ restroù n'eo ket aotreet gant sontadegoù.",
"upload_form.audio_description": "Diskrivañ evit tud a zo kollet o c'hlev",
"upload_form.description": "Diskrivañ evit tud a zo kollet o gweled",
"upload_form.edit": "Aozañ",
"upload_form.thumbnail": "Kemmañ ar velvenn",
"upload_form.undo": "Dilemel",
"upload_form.video_description": "Diskrivañ evit tud a zo kollet o gweled pe o c'hlev",
"upload_modal.analyzing_picture": "Analyzing picture…",
"upload_modal.analyzing_picture": "O tielfennañ ar skeudenn…",
"upload_modal.apply": "Arloañ",
"upload_modal.applying": "Applying…",
"upload_modal.applying": "Oc'h arloañ…",
"upload_modal.choose_image": "Dibab ur skeudenn",
"upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
"upload_modal.description_placeholder": "Ul louarn gell mibin a zo o lammat a-zioc'h ar c'hi lezirek",
"upload_modal.detect_text": "Dinoiñ testenn diouzh ar skeudenn",
"upload_modal.edit_media": "Embann ar media",
"upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
"upload_modal.preparing_ocr": "Preparing OCR…",
"upload_modal.hint": "Klikit pe tennit ar c'helc'h war ar rakwel evit dibab al lerc'h kengreizel a vo gwelet atav war an holl melvennoù.",
"upload_modal.preparing_ocr": "Oc'h aozañ OCR…",
"upload_modal.preview_label": "Rakwel ({ratio})",
"upload_progress.label": "O pellgargañ...",
"video.close": "Serriñ ar video",
"video.download": "Pellgargañ ar restr",
"video.exit_fullscreen": "Kuitaat ar mod skramm leun",
"video.expand": "Expand video",
"video.expand": "Ledanaat ar video",
"video.fullscreen": "Skramm a-bezh",
"video.hide": "Kuzhat ar video",
"video.mute": "Paouez gant ar son",
"video.pause": "Pause",
"video.pause": "Paouez",
"video.play": "Lenn",
"video.unmute": "Lakaat ar son en-dro"
}

View File

@ -47,16 +47,17 @@
"account.unmute": "Treure silenci de @{name}",
"account.unmute_notifications": "Activar notificacions de @{name}",
"account_note.placeholder": "Fes clic per afegir una nota",
"admin.dashboard.retention": "Retention",
"admin.dashboard.retention.average": "Average",
"admin.dashboard.retention.cohort": "Sign-up month",
"admin.dashboard.retention.cohort_size": "New users",
"admin.dashboard.daily_retention": "User retention rate by day after sign-up",
"admin.dashboard.monthly_retention": "User retention rate by month after sign-up",
"admin.dashboard.retention.average": "Mitjana",
"admin.dashboard.retention.cohort": "Registres mes",
"admin.dashboard.retention.cohort_size": "Nous usuaris",
"alert.rate_limited.message": "Si us plau torna-ho a provar després de {retry_time, time, medium}.",
"alert.rate_limited.title": "Límit de freqüència",
"alert.unexpected.message": "S'ha produït un error inesperat.",
"alert.unexpected.title": "Vaja!",
"announcement.announcement": "Anunci",
"attachments_list.unprocessed": "(unprocessed)",
"attachments_list.unprocessed": "(sense processar)",
"autosuggest_hashtag.per_week": "{count} per setmana",
"boost_modal.combo": "Pots prémer {combo} per saltar-te això el proper cop",
"bundle_column_error.body": "S'ha produït un error en carregar aquest component.",
@ -96,14 +97,15 @@
"compose_form.lock_disclaimer": "El teu compte no està bloquejat {locked}. Tothom pot seguir-te i veure els teus missatges a seguidors.",
"compose_form.lock_disclaimer.lock": "bloquejat",
"compose_form.placeholder": "En què penses?",
"compose_form.poll.add_option": "Afegeix una opció",
"compose_form.poll.add_option": "Afegir una opció",
"compose_form.poll.duration": "Durada de l'enquesta",
"compose_form.poll.option_placeholder": "Opció {number}",
"compose_form.poll.remove_option": "Elimina aquesta opció",
"compose_form.poll.switch_to_multiple": "Canvia lenquesta per a permetre diverses opcions",
"compose_form.poll.switch_to_single": "Canvia lenquesta per a permetre una única opció",
"compose_form.publish": "Tut",
"compose_form.publish": "Publicar",
"compose_form.publish_loud": "{publish}!",
"compose_form.save_changes": "Save changes",
"compose_form.sensitive.hide": "Marcar mèdia com a sensible",
"compose_form.sensitive.marked": "Mèdia marcat com a sensible",
"compose_form.sensitive.unmarked": "Mèdia no està marcat com a sensible",
@ -118,8 +120,8 @@
"confirmations.delete.message": "Estàs segur que vols suprimir aquest tut?",
"confirmations.delete_list.confirm": "Suprimeix",
"confirmations.delete_list.message": "Estàs segur que vols suprimir permanentment aquesta llista?",
"confirmations.discard_edit_media.confirm": "Discard",
"confirmations.discard_edit_media.message": "You have unsaved changes to the media description or preview, discard them anyway?",
"confirmations.discard_edit_media.confirm": "Descarta",
"confirmations.discard_edit_media.message": "Tens canvis no desats de la descripciò de mèdia o previsualització, els vols descartar?",
"confirmations.domain_block.confirm": "Amaga tot el domini",
"confirmations.domain_block.message": "Estàs segur, realment segur que vols bloquejar totalment {domain}? En la majoria dels casos bloquejar o silenciar uns pocs objectius és suficient i preferible. No veuràs contingut daquest domini en cap de les línies de temps ni en les notificacions. Els teus seguidors daquest domini seran eliminats.",
"confirmations.logout.confirm": "Tancar sessió",
@ -244,7 +246,7 @@
"keyboard_shortcuts.search": "per a centrar la cerca",
"keyboard_shortcuts.spoilers": "mostrar/amagar el camp CW",
"keyboard_shortcuts.start": "per a obrir la columna \"Començar\"",
"keyboard_shortcuts.toggle_hidden": "per a mostrar o amagar text sota CW",
"keyboard_shortcuts.toggle_hidden": "Mostrar/ocultar el text marcat com a sensible",
"keyboard_shortcuts.toggle_sensitivity": "per a mostrar o amagar contingut multimèdia",
"keyboard_shortcuts.toot": "per a començar un tut nou de trinca",
"keyboard_shortcuts.unfocus": "descentrar l'àrea de composició de text/cerca",
@ -281,14 +283,14 @@
"navigation_bar.community_timeline": "Línia de temps Local",
"navigation_bar.compose": "Redacta un nou tut",
"navigation_bar.direct": "Missatges directes",
"navigation_bar.discover": "Descobreix",
"navigation_bar.discover": "Descobrir",
"navigation_bar.domain_blocks": "Dominis ocults",
"navigation_bar.edit_profile": "Editar perfil",
"navigation_bar.favourites": "Preferits",
"navigation_bar.filters": "Paraules silenciades",
"navigation_bar.follow_requests": "Sol·licituds de seguiment",
"navigation_bar.follows_and_followers": "Seguits i seguidors",
"navigation_bar.info": "Sobre aquest servidor",
"navigation_bar.info": "Quant a aquest servidor",
"navigation_bar.keyboard_shortcuts": "Dreceres de teclat",
"navigation_bar.lists": "Llistes",
"navigation_bar.logout": "Tancar sessió",
@ -312,7 +314,7 @@
"notifications.column_settings.favourite": "Preferits:",
"notifications.column_settings.filter_bar.advanced": "Mostra totes les categories",
"notifications.column_settings.filter_bar.category": "Barra ràpida de filtres",
"notifications.column_settings.filter_bar.show": "Mostra",
"notifications.column_settings.filter_bar.show_bar": "Mostra la barra de filtres",
"notifications.column_settings.follow": "Nous seguidors:",
"notifications.column_settings.follow_request": "Nova sol·licitud de seguiment:",
"notifications.column_settings.mention": "Mencions:",
@ -322,7 +324,8 @@
"notifications.column_settings.show": "Mostra en la columna",
"notifications.column_settings.sound": "Reproduir so",
"notifications.column_settings.status": "Nous tuts:",
"notifications.column_settings.unread_markers.category": "Marcadors de notificacions no llegides",
"notifications.column_settings.unread_notifications.category": "Notificacions no llegides",
"notifications.column_settings.unread_notifications.highlight": "Destaca notificacions no llegides",
"notifications.filter.all": "Tots",
"notifications.filter.boosts": "Impulsos",
"notifications.filter.favourites": "Favorits",
@ -346,10 +349,10 @@
"poll.total_votes": "{count, plural, one {# vot} other {# vots}}",
"poll.vote": "Vota",
"poll.voted": "Vas votar per aquesta resposta",
"poll.votes": "{votes, plural, one {# vote} other {# votes}}",
"poll_button.add_poll": "Afegeix una enquesta",
"poll.votes": "{votes, plural, one {# vot} other {# vots}}",
"poll_button.add_poll": "Afegir una enquesta",
"poll_button.remove_poll": "Elimina l'enquesta",
"privacy.change": "Ajusta l'estat de privacitat",
"privacy.change": "Ajustar l'estat de privacitat",
"privacy.direct.long": "Publicar només per als usuaris esmentats",
"privacy.direct.short": "Directe",
"privacy.private.long": "Publicar només a seguidors",
@ -362,12 +365,20 @@
"regeneration_indicator.label": "Carregant…",
"regeneration_indicator.sublabel": "S'està preparant la línia de temps Inici!",
"relative_time.days": "fa {number} dies",
"relative_time.full.days": "{number, plural, one {# day} other {# days}} ago",
"relative_time.full.hours": "{number, plural, one {# hour} other {# hours}} ago",
"relative_time.full.just_now": "just now",
"relative_time.full.minutes": "{number, plural, one {# minute} other {# minutes}} ago",
"relative_time.full.seconds": "{number, plural, one {# second} other {# seconds}} ago",
"relative_time.hours": "fa {number} hores",
"relative_time.just_now": "ara",
"relative_time.minutes": "fa {number} minuts",
"relative_time.seconds": "fa {number} segons",
"relative_time.today": "avui",
"reply_indicator.cancel": "Cancel·lar",
"report.categories.other": "Other",
"report.categories.spam": "Spam",
"report.categories.violation": "Content violates one or more server rules",
"report.forward": "Reenvia a {target}",
"report.forward_hint": "Aquest compte és d'un altre servidor. Enviar-hi també una copia anònima del informe?",
"report.hint": "El informe s'enviarà als moderadors del teu servidor. Pots explicar perquè vols informar d'aquest compte aquí:",
@ -396,9 +407,14 @@
"status.delete": "Esborrar",
"status.detailed_status": "Visualització detallada de la conversa",
"status.direct": "Missatge directe @{name}",
"status.edit": "Edit",
"status.edited": "Edited {date}",
"status.edited_x_times": "Edited {count, plural, one {{count} time} other {{count} times}}",
"status.embed": "Incrustar",
"status.favourite": "Favorit",
"status.filtered": "Filtrat",
"status.history.created": "{name} created {date}",
"status.history.edited": "{name} edited {date}",
"status.load_more": "Carrega més",
"status.media_hidden": "Multimèdia amagat",
"status.mention": "Esmentar @{name}",
@ -430,7 +446,7 @@
"status.unpin": "Deslliga del perfil",
"suggestions.dismiss": "Descartar suggeriment",
"suggestions.header": "És possible que estiguis interessat en…",
"tabs_bar.federated_timeline": "Federada",
"tabs_bar.federated_timeline": "Federat",
"tabs_bar.home": "Inici",
"tabs_bar.local_timeline": "Local",
"tabs_bar.notifications": "Notificacions",
@ -462,7 +478,7 @@
"upload_form.video_description": "Descriu per a les persones amb pèrdua auditiva o deficiència visual",
"upload_modal.analyzing_picture": "Analitzant imatge…",
"upload_modal.apply": "Aplica",
"upload_modal.applying": "Applying…",
"upload_modal.applying": "Aplicant…",
"upload_modal.choose_image": "Tria imatge",
"upload_modal.description_placeholder": "Jove xef, porti whisky amb quinze glaçons dhidrogen, coi!",
"upload_modal.detect_text": "Detecta el text de l'imatge",

View File

@ -47,7 +47,8 @@
"account.unmute": "Ùn piattà più @{name}",
"account.unmute_notifications": "Ùn piattà più nutificazione da @{name}",
"account_note.placeholder": "Senza cummentariu",
"admin.dashboard.retention": "Retention",
"admin.dashboard.daily_retention": "User retention rate by day after sign-up",
"admin.dashboard.monthly_retention": "User retention rate by month after sign-up",
"admin.dashboard.retention.average": "Average",
"admin.dashboard.retention.cohort": "Sign-up month",
"admin.dashboard.retention.cohort_size": "New users",
@ -56,7 +57,7 @@
"alert.unexpected.message": "Un prublemu inaspettatu hè accadutu.",
"alert.unexpected.title": "Uups!",
"announcement.announcement": "Annunziu",
"attachments_list.unprocessed": "(unprocessed)",
"attachments_list.unprocessed": "(micca trattata)",
"autosuggest_hashtag.per_week": "{count} per settimana",
"boost_modal.combo": "Pudete appughjà nant'à {combo} per saltà quessa a prussima volta",
"bundle_column_error.body": "C'hè statu un prublemu caricandu st'elementu.",
@ -104,6 +105,7 @@
"compose_form.poll.switch_to_single": "Cambià u scandagliu per ùn accittà ch'una scelta",
"compose_form.publish": "Toot",
"compose_form.publish_loud": "{publish}!",
"compose_form.save_changes": "Save changes",
"compose_form.sensitive.hide": "{count, plural, one {Indicà u media cum'è sensibile} other {Indicà i media cum'è sensibili}}",
"compose_form.sensitive.marked": "{count, plural, one {Media indicatu cum'è sensibile} other {Media indicati cum'è sensibili}}",
"compose_form.sensitive.unmarked": "{count, plural, one {Media micca indicatu cum'è sensibile} other {Media micca indicati cum'è sensibili}}",
@ -118,7 +120,7 @@
"confirmations.delete.message": "Site sicuru·a che vulete sguassà stu statutu?",
"confirmations.delete_list.confirm": "Toglie",
"confirmations.delete_list.message": "Site sicuru·a che vulete toglie sta lista?",
"confirmations.discard_edit_media.confirm": "Discard",
"confirmations.discard_edit_media.confirm": "Scartà",
"confirmations.discard_edit_media.message": "You have unsaved changes to the media description or preview, discard them anyway?",
"confirmations.domain_block.confirm": "Piattà tuttu u duminiu",
"confirmations.domain_block.message": "Site veramente sicuru·a che vulete piattà tuttu à {domain}? Saria forse abbastanza di bluccà ò piattà alcuni conti da quallà. Ùn viderete più nunda da quallà indè e linee pubbliche o e nutificazione. I vostri abbunati da stu duminiu saranu tolti.",
@ -312,7 +314,7 @@
"notifications.column_settings.favourite": "Favuriti:",
"notifications.column_settings.filter_bar.advanced": "Affissà tutte e categurie",
"notifications.column_settings.filter_bar.category": "Barra di ricerca pronta",
"notifications.column_settings.filter_bar.show": "Mustrà",
"notifications.column_settings.filter_bar.show_bar": "Show filter bar",
"notifications.column_settings.follow": "Abbunati novi:",
"notifications.column_settings.follow_request": "Nove dumande d'abbunamentu:",
"notifications.column_settings.mention": "Minzione:",
@ -322,7 +324,8 @@
"notifications.column_settings.show": "Mustrà indè a colonna",
"notifications.column_settings.sound": "Sunà",
"notifications.column_settings.status": "Statuti novi:",
"notifications.column_settings.unread_markers.category": "Marcatori di nutificazione micca lette",
"notifications.column_settings.unread_notifications.category": "Unread notifications",
"notifications.column_settings.unread_notifications.highlight": "Highlight unread notifications",
"notifications.filter.all": "Tuttu",
"notifications.filter.boosts": "Spartere",
"notifications.filter.favourites": "Favuriti",
@ -346,7 +349,7 @@
"poll.total_votes": "{count, plural, one {# votu} other {# voti}}",
"poll.vote": "Vutà",
"poll.voted": "Avete vutatu per sta risposta",
"poll.votes": "{votes, plural, one {# vote} other {# votes}}",
"poll.votes": "{votes, plural, one {# votu} other {# voti}}",
"poll_button.add_poll": "Aghjunghje",
"poll_button.remove_poll": "Toglie u scandagliu",
"privacy.change": "Mudificà a cunfidenzialità di u statutu",
@ -362,12 +365,20 @@
"regeneration_indicator.label": "Caricamentu…",
"regeneration_indicator.sublabel": "Priparazione di a vostra pagina d'accolta!",
"relative_time.days": "{number}ghj",
"relative_time.full.days": "{number, plural, one {# day} other {# days}} ago",
"relative_time.full.hours": "{number, plural, one {# hour} other {# hours}} ago",
"relative_time.full.just_now": "just now",
"relative_time.full.minutes": "{number, plural, one {# minute} other {# minutes}} ago",
"relative_time.full.seconds": "{number, plural, one {# second} other {# seconds}} ago",
"relative_time.hours": "{number}o",
"relative_time.just_now": "avà",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"relative_time.today": "oghji",
"reply_indicator.cancel": "Annullà",
"report.categories.other": "Other",
"report.categories.spam": "Spam",
"report.categories.violation": "Content violates one or more server rules",
"report.forward": "Trasferisce à {target}",
"report.forward_hint": "U contu hè nant'à un'altru servore. Vulete ancu mandà una copia anonima di u signalamentu quallà?",
"report.hint": "U signalamentu sarà mandatu à i muderatori di u servore. Pudete spiegà perchè avete palisatu stu contu quì sottu:",
@ -396,9 +407,14 @@
"status.delete": "Toglie",
"status.detailed_status": "Vista in ditagliu di a cunversazione",
"status.direct": "Mandà un missaghju @{name}",
"status.edit": "Edit",
"status.edited": "Edited {date}",
"status.edited_x_times": "Edited {count, plural, one {{count} time} other {{count} times}}",
"status.embed": "Integrà",
"status.favourite": "Aghjunghje à i favuriti",
"status.filtered": "Filtratu",
"status.history.created": "{name} created {date}",
"status.history.edited": "{name} edited {date}",
"status.load_more": "Vede di più",
"status.media_hidden": "Media piattata",
"status.mention": "Mintuvà @{name}",
@ -462,7 +478,7 @@
"upload_form.video_description": "Discrizzione per i ciochi o cechi",
"upload_modal.analyzing_picture": "Analisi di u ritrattu…",
"upload_modal.apply": "Affettà",
"upload_modal.applying": "Applying…",
"upload_modal.applying": "Appiegazione…",
"upload_modal.choose_image": "Cambià ritrattu",
"upload_modal.description_placeholder": "Chì tempi brevi ziu, quandu solfeghji",
"upload_modal.detect_text": "Ditettà testu da u ritrattu",

View File

@ -12,7 +12,7 @@
"account.disable_notifications": "Zrušit upozorňování na příspěvky @{name}",
"account.domain_blocked": "Doména blokována",
"account.edit_profile": "Upravit profil",
"account.enable_notifications": "Oznámit mě na příspěvky @{name}",
"account.enable_notifications": "Oznamovat mi příspěvky @{name}",
"account.endorse": "Zvýraznit na profilu",
"account.follow": "Sledovat",
"account.followers": "Sledující",
@ -47,16 +47,17 @@
"account.unmute": "Zrušit skrytí @{name}",
"account.unmute_notifications": "Zrušit skrytí oznámení od @{name}",
"account_note.placeholder": "Klikněte pro přidání poznámky",
"admin.dashboard.retention": "Retention",
"admin.dashboard.retention.average": "Average",
"admin.dashboard.retention.cohort": "Sign-up month",
"admin.dashboard.retention.cohort_size": "New users",
"admin.dashboard.daily_retention": "User retention rate by day after sign-up",
"admin.dashboard.monthly_retention": "User retention rate by month after sign-up",
"admin.dashboard.retention.average": "Průměr",
"admin.dashboard.retention.cohort": "Měsíc registrace",
"admin.dashboard.retention.cohort_size": "Noví uživatelé",
"alert.rate_limited.message": "Zkuste to prosím znovu za {retry_time, time, medium}.",
"alert.rate_limited.title": "Spojení omezena",
"alert.unexpected.message": "Objevila se neočekávaná chyba.",
"alert.unexpected.title": "Jejda!",
"announcement.announcement": "Oznámení",
"attachments_list.unprocessed": "(unprocessed)",
"attachments_list.unprocessed": "(nezpracováno)",
"autosuggest_hashtag.per_week": "{count} za týden",
"boost_modal.combo": "Příště můžete pro přeskočení stisknout {combo}",
"bundle_column_error.body": "Při načítání této komponenty se něco pokazilo.",
@ -104,6 +105,7 @@
"compose_form.poll.switch_to_single": "Povolit u ankety výběr jediné možnosti",
"compose_form.publish": "Odeslat",
"compose_form.publish_loud": "{publish}!",
"compose_form.save_changes": "Save changes",
"compose_form.sensitive.hide": "{count, plural, one {Označit média za citlivá} few {Označit média za citlivá} many {Označit média za citlivá} other {Označit média za citlivá}}",
"compose_form.sensitive.marked": "{count, plural, one {Média jsou označena za citlivá} few {Média jsou označena za citlivá} many {Média jsou označena za citlivá} other {Média jsou označena za citlivá}}",
"compose_form.sensitive.unmarked": "{count, plural, one {Média nejsou označena za citlivá} few {Média nejsou označena za citlivá} many {Média nejsou označena za citlivá} other {Média nejsou označena za citlivá}}",
@ -118,8 +120,8 @@
"confirmations.delete.message": "Opravdu chcete smazat tento příspěvek?",
"confirmations.delete_list.confirm": "Smazat",
"confirmations.delete_list.message": "Opravdu chcete tento seznam navždy smazat?",
"confirmations.discard_edit_media.confirm": "Discard",
"confirmations.discard_edit_media.message": "You have unsaved changes to the media description or preview, discard them anyway?",
"confirmations.discard_edit_media.confirm": "Zahodit",
"confirmations.discard_edit_media.message": "Máte neuložené změny popisku médií nebo náhledu, přesto je zahodit?",
"confirmations.domain_block.confirm": "Blokovat celou doménu",
"confirmations.domain_block.message": "Opravdu chcete blokovat celou doménu {domain}? Ve většině případů stačí zablokovat nebo skrýt pár konkrétních uživatelů, což také doporučujeme. Z této domény neuvidíte obsah v žádné veřejné časové ose ani v oznámeních. Vaši sledující z této domény budou odstraněni.",
"confirmations.logout.confirm": "Odhlásit",
@ -312,7 +314,7 @@
"notifications.column_settings.favourite": "Oblíbení:",
"notifications.column_settings.filter_bar.advanced": "Zobrazit všechny kategorie",
"notifications.column_settings.filter_bar.category": "Panel rychlého filtrování",
"notifications.column_settings.filter_bar.show": "Zobrazit",
"notifications.column_settings.filter_bar.show_bar": "Zobrazit panel filtrů",
"notifications.column_settings.follow": "Noví sledující:",
"notifications.column_settings.follow_request": "Nové žádosti o sledování:",
"notifications.column_settings.mention": "Zmínky:",
@ -322,7 +324,8 @@
"notifications.column_settings.show": "Zobrazit ve sloupci",
"notifications.column_settings.sound": "Přehrát zvuk",
"notifications.column_settings.status": "Nové příspěvky:",
"notifications.column_settings.unread_markers.category": "Značky nepřečtených oznámení",
"notifications.column_settings.unread_notifications.category": "Nepřečtená oznámení",
"notifications.column_settings.unread_notifications.highlight": "Zvýraznit nepřečtená oznámení",
"notifications.filter.all": "Vše",
"notifications.filter.boosts": "Boosty",
"notifications.filter.favourites": "Oblíbení",
@ -346,7 +349,7 @@
"poll.total_votes": "{count, plural, one {# hlas} few {# hlasy} many {# hlasů} other {# hlasů}}",
"poll.vote": "Hlasovat",
"poll.voted": "Pro tuto odpověď jste hlasovali",
"poll.votes": "{votes, plural, one {# vote} other {# votes}}",
"poll.votes": "{votes, plural, one {# hlas} few {# hlasy} many {# hlasů} other {# hlasů}}",
"poll_button.add_poll": "Přidat anketu",
"poll_button.remove_poll": "Odstranit anketu",
"privacy.change": "Změnit soukromí příspěvku",
@ -362,12 +365,20 @@
"regeneration_indicator.label": "Načítání…",
"regeneration_indicator.sublabel": "Váš domovský kanál se připravuje!",
"relative_time.days": "{number} d",
"relative_time.full.days": "{number, plural, one {# day} other {# days}} ago",
"relative_time.full.hours": "{number, plural, one {# hour} other {# hours}} ago",
"relative_time.full.just_now": "just now",
"relative_time.full.minutes": "{number, plural, one {# minute} other {# minutes}} ago",
"relative_time.full.seconds": "{number, plural, one {# second} other {# seconds}} ago",
"relative_time.hours": "{number} h",
"relative_time.just_now": "teď",
"relative_time.minutes": "{number} m",
"relative_time.seconds": "{number} s",
"relative_time.today": "dnes",
"reply_indicator.cancel": "Zrušit",
"report.categories.other": "Other",
"report.categories.spam": "Spam",
"report.categories.violation": "Content violates one or more server rules",
"report.forward": "Přeposlat na {target}",
"report.forward_hint": "Tento účet je z jiného serveru. Chcete na něj také poslat anonymizovanou kopii hlášení?",
"report.hint": "Hlášení bude zasláno moderátorům vašeho serveru. Níže můžete uvést, proč tento účet nahlašujete:",
@ -396,9 +407,14 @@
"status.delete": "Smazat",
"status.detailed_status": "Podrobné zobrazení konverzace",
"status.direct": "Poslat @{name} přímou zprávu",
"status.edit": "Edit",
"status.edited": "Edited {date}",
"status.edited_x_times": "Edited {count, plural, one {{count} time} other {{count} times}}",
"status.embed": "Vložit na web",
"status.favourite": "Oblíbit",
"status.filtered": "Filtrováno",
"status.history.created": "{name} created {date}",
"status.history.edited": "{name} edited {date}",
"status.load_more": "Zobrazit více",
"status.media_hidden": "Média skryta",
"status.mention": "Zmínit @{name}",
@ -461,8 +477,8 @@
"upload_form.undo": "Smazat",
"upload_form.video_description": "Popis pro sluchově či zrakově postižené",
"upload_modal.analyzing_picture": "Analyzuji obrázek…",
"upload_modal.apply": "Použít",
"upload_modal.applying": "Applying…",
"upload_modal.apply": "Aplikovat",
"upload_modal.applying": "Aplikuji…",
"upload_modal.choose_image": "Vybrat obrázek",
"upload_modal.description_placeholder": "Příliš žluťoučký kůň úpěl ďábelské ódy",
"upload_modal.detect_text": "Detekovat text z obrázku",

View File

@ -47,7 +47,8 @@
"account.unmute": "Dad-dawelu @{name}",
"account.unmute_notifications": "Dad-dawelu hysbysiadau o @{name}",
"account_note.placeholder": "Clicio i ychwanegu nodyn",
"admin.dashboard.retention": "Retention",
"admin.dashboard.daily_retention": "User retention rate by day after sign-up",
"admin.dashboard.monthly_retention": "User retention rate by month after sign-up",
"admin.dashboard.retention.average": "Average",
"admin.dashboard.retention.cohort": "Sign-up month",
"admin.dashboard.retention.cohort_size": "New users",
@ -104,6 +105,7 @@
"compose_form.poll.switch_to_single": "Newid pleidlais i gyfyngu i un dewis",
"compose_form.publish": "Tŵt",
"compose_form.publish_loud": "{publish}!",
"compose_form.save_changes": "Save changes",
"compose_form.sensitive.hide": "Marcio cyfryngau fel eu bod yn sensitif",
"compose_form.sensitive.marked": "Cyfryngau wedi'u marcio'n sensitif",
"compose_form.sensitive.unmarked": "Nid yw'r cyfryngau wedi'u marcio'n sensitif",
@ -312,7 +314,7 @@
"notifications.column_settings.favourite": "Ffefrynnau:",
"notifications.column_settings.filter_bar.advanced": "Dangos pob categori",
"notifications.column_settings.filter_bar.category": "Bar hidlo",
"notifications.column_settings.filter_bar.show": "Dangos",
"notifications.column_settings.filter_bar.show_bar": "Show filter bar",
"notifications.column_settings.follow": "Dilynwyr newydd:",
"notifications.column_settings.follow_request": "Ceisiadau dilyn newydd:",
"notifications.column_settings.mention": "Crybwylliadau:",
@ -322,7 +324,8 @@
"notifications.column_settings.show": "Dangos yn y golofn",
"notifications.column_settings.sound": "Chwarae sain",
"notifications.column_settings.status": "New toots:",
"notifications.column_settings.unread_markers.category": "Unread notification markers",
"notifications.column_settings.unread_notifications.category": "Unread notifications",
"notifications.column_settings.unread_notifications.highlight": "Highlight unread notifications",
"notifications.filter.all": "Pob",
"notifications.filter.boosts": "Hybiadau",
"notifications.filter.favourites": "Ffefrynnau",
@ -362,12 +365,20 @@
"regeneration_indicator.label": "Llwytho…",
"regeneration_indicator.sublabel": "Mae eich ffrwd cartref yn cael ei baratoi!",
"relative_time.days": "{number}dydd",
"relative_time.full.days": "{number, plural, one {# day} other {# days}} ago",
"relative_time.full.hours": "{number, plural, one {# hour} other {# hours}} ago",
"relative_time.full.just_now": "just now",
"relative_time.full.minutes": "{number, plural, one {# minute} other {# minutes}} ago",
"relative_time.full.seconds": "{number, plural, one {# second} other {# seconds}} ago",
"relative_time.hours": "{number}awr",
"relative_time.just_now": "nawr",
"relative_time.minutes": "{number}munud",
"relative_time.seconds": "{number}eiliad",
"relative_time.today": "heddiw",
"reply_indicator.cancel": "Canslo",
"report.categories.other": "Other",
"report.categories.spam": "Spam",
"report.categories.violation": "Content violates one or more server rules",
"report.forward": "Ymlaen i {target}",
"report.forward_hint": "Mae'r cyfrif o weinydd arall. Anfon copi anhysbys o'r adroddiad yno hefyd?",
"report.hint": "Bydd yr adroddiad yn cael ei anfon i arolygydd eich achos. Mae modd darparu esboniad o pam yr ydych yn cwyno am y cyfrif hwn isod:",
@ -396,9 +407,14 @@
"status.delete": "Dileu",
"status.detailed_status": "Golwg manwl o'r sgwrs",
"status.direct": "Neges breifat @{name}",
"status.edit": "Edit",
"status.edited": "Edited {date}",
"status.edited_x_times": "Edited {count, plural, one {{count} time} other {{count} times}}",
"status.embed": "Plannu",
"status.favourite": "Hoffi",
"status.filtered": "Wedi'i hidlo",
"status.history.created": "{name} created {date}",
"status.history.edited": "{name} edited {date}",
"status.load_more": "Llwythwch mwy",
"status.media_hidden": "Cyfryngau wedi'u cuddio",
"status.mention": "Crybwyll @{name}",

View File

@ -1,75 +1,76 @@
{
"account.account_note_header": "Notat",
"account.account_note_header": "Note",
"account.add_or_remove_from_list": "Tilføj eller fjern fra lister",
"account.badges.bot": "Bot",
"account.badges.group": "Gruppe",
"account.block": "Blokér @{name}",
"account.block_domain": "Blokér domænet {domain}",
"account.blocked": "Blokeret",
"account.browse_more_on_origin_server": "Gennemse mere på den oprindelige profil",
"account.cancel_follow_request": "Annullér følgeranmodning",
"account.browse_more_on_origin_server": "Se mere på den oprindelige profil",
"account.cancel_follow_request": "Annullér følgeanmodning",
"account.direct": "Direkte besked til @{name}",
"account.disable_notifications": "Advisér mig ikke længere, når @{name} poster",
"account.domain_blocked": "Domæne blokeret",
"account.edit_profile": "Redigere profil",
"account.enable_notifications": "Advisér mig, når @{name} poster",
"account.edit_profile": "Redigér profil",
"account.enable_notifications": "Giv besked når @{name} udgiver nyt",
"account.endorse": "Fremhæv på profil",
"account.follow": "Følg",
"account.followers": "Følgere",
"account.followers.empty": "Ingen følger denne bruger endnu.",
"account.followers.empty": "Ingen følger brugeren endnu.",
"account.followers_counter": "{count, plural, one {{counter} Følger} other {{counter} Følgere}}",
"account.following_counter": "{count, plural, one {{counter} Følger} other {{counter} Følgere}}",
"account.follows.empty": "Denne bruger følger endnu ikke nogen.",
"account.following_counter": "{count, plural, one {{counter} Følges} other {{counter} Følges}}",
"account.follows.empty": "Denne bruger følger ikke nogen endnu.",
"account.follows_you": "Følger dig",
"account.hide_reblogs": "Skjul fremhævelserne fra @{name}",
"account.hide_reblogs": "Skjul fremhævelser fra @{name}",
"account.joined": "Tilmeldt {date}",
"account.last_status": "Senest aktiv",
"account.link_verified_on": "Ejerskab af dette link blev tjekket {date}",
"account.locked_info": "Denne kontos fortrolighedsstatus er sat til låst. Ejeren bedømmer manuelt, hvem der kan følge vedkommende.",
"account.media": "Medie",
"account.media": "Medier",
"account.mention": "Nævn @{name}",
"account.moved_to": "{name} er flyttet til:",
"account.mute": "Tavsgør @{name}",
"account.mute_notifications": "Tavsgør notifikationer fra @{name}",
"account.muted": "Tavsgjort",
"account.mute": "Skjul @{name}",
"account.mute_notifications": "Skjul notifikationer fra @{name}",
"account.muted": "Tystnet",
"account.never_active": "Aldrig",
"account.posts": "Trut",
"account.posts_with_replies": "Trut og svar",
"account.posts": "Indlæg",
"account.posts_with_replies": "Indlæg og svar",
"account.report": "Anmeld @{name}",
"account.requested": "Afventer godkendelse. Tryk for at annullere følgeanmodning",
"account.share": "Del @{name}s profil",
"account.show_reblogs": "Vis fremhævelserne fra @{name}",
"account.statuses_counter": "{count, plural, one {{counter} Trut} other {{counter} Trut}}",
"account.show_reblogs": "Vis fremhævelser fra @{name}",
"account.statuses_counter": "{count, plural, one {{counter} Indlæg} other {{counter} Indlæg}}",
"account.unblock": "Fjern blokeringen af @{name}",
"account.unblock_domain": "Afblokér domænet {domain}",
"account.unendorse": "Fremhæv ikke på profil",
"account.unendorse": "Fjern visning på din profil",
"account.unfollow": "Følg ikke længere",
"account.unmute": "Fjern tavsgjort for @{name}",
"account.unmute_notifications": "Fjern tavsgjort for notifikationer fra @{name}",
"account.unmute_notifications": "Slå notifikationer om @{name} til igen",
"account_note.placeholder": "Klik for at tilføje notat",
"admin.dashboard.retention": "Retention",
"admin.dashboard.retention.average": "Average",
"admin.dashboard.retention.cohort": "Sign-up month",
"admin.dashboard.retention.cohort_size": "New users",
"admin.dashboard.daily_retention": "User retention rate by day after sign-up",
"admin.dashboard.monthly_retention": "User retention rate by month after sign-up",
"admin.dashboard.retention.average": "Gennemsnitlig",
"admin.dashboard.retention.cohort": "Tilmeldingsmåned",
"admin.dashboard.retention.cohort_size": "Nye brugere",
"alert.rate_limited.message": "Forsøg igen efter {retry_time, time, medium}.",
"alert.rate_limited.title": "Gradsbegrænset",
"alert.unexpected.message": "En uventet fejl opstod.",
"alert.rate_limited.title": "Hastighedsbegrænset",
"alert.unexpected.message": "Der skete en uventet fejl.",
"alert.unexpected.title": "Ups!",
"announcement.announcement": "Bekendtgørelse",
"attachments_list.unprocessed": "(unprocessed)",
"attachments_list.unprocessed": "(ubearbejdet)",
"autosuggest_hashtag.per_week": "{count} pr. uge",
"boost_modal.combo": "Du kan trykke på {combo} for at overspringe dette næste gang",
"boost_modal.combo": "Du kan trykke på {combo} for at springe over næste gang",
"bundle_column_error.body": "Noget gik galt under indlæsningen af denne komponent.",
"bundle_column_error.retry": "Forsøg igen",
"bundle_column_error.retry": "Prøv igen",
"bundle_column_error.title": "Netværksfejl",
"bundle_modal_error.close": "Luk",
"bundle_modal_error.message": "Noget gik galt under indlæsningen af denne komponent.",
"bundle_modal_error.retry": "Forsøg igen",
"bundle_modal_error.retry": "Prøv igen",
"column.blocks": "Blokerede brugere",
"column.bookmarks": "Bogmærker",
"column.community": "Lokal tidslinje",
"column.direct": "Direkte beskeder",
"column.directory": "Gennemse profiler",
"column.directory": "Se profiler",
"column.domain_blocks": "Blokerede domæner",
"column.favourites": "Favoritter",
"column.follow_requests": "Følg-anmodninger",
@ -77,8 +78,8 @@
"column.lists": "Lister",
"column.mutes": "Tavsgjorte brugere",
"column.notifications": "Notifikationer",
"column.pins": "Fastgjorte trut",
"column.public": "Forenede tidslinje",
"column.pins": "Fastgjorte indlæg",
"column.public": "Fælles tidslinje",
"column_back_button.label": "Tilbage",
"column_header.hide_settings": "Skjul indstillinger",
"column_header.moveLeft_settings": "Flyt kolonne til venstre",
@ -88,71 +89,72 @@
"column_header.unpin": "Løsgør",
"column_subheading.settings": "Indstillinger",
"community.column_settings.local_only": "Kun lokalt",
"community.column_settings.media_only": "Kun medie",
"community.column_settings.remote_only": "Kun fjernt",
"compose_form.direct_message_warning": "Dette trut sendes kun til de nævnte brugere.",
"community.column_settings.media_only": "Kun medier",
"community.column_settings.remote_only": "Kun udefra",
"compose_form.direct_message_warning": "Indlægget bliver kun sendt til de nævnte brugere.",
"compose_form.direct_message_warning_learn_more": "Få mere at vide",
"compose_form.hashtag_warning": "Dette trut vises ikke under noget hashtag, da det ikke er listet. Kun offentlige trut kan søges via hashtags.",
"compose_form.lock_disclaimer": "Din konto er ikke {locked}. Alle kan følge dig for at se dine kun-følger poster.",
"compose_form.lock_disclaimer": "Din konto er ikke {locked}. Alle kan følge dig, så de også kan se de indlæg, der kun er til følgere.",
"compose_form.lock_disclaimer.lock": "låst",
"compose_form.placeholder": "Hvad tænker du på?",
"compose_form.placeholder": "Hvad vil du gerne fortælle om?",
"compose_form.poll.add_option": "Tilføj valgmulighed",
"compose_form.poll.duration": "Afstemningsvarighed",
"compose_form.poll.duration": "Afstemningens varighed",
"compose_form.poll.option_placeholder": "Valgmulighed {number}",
"compose_form.poll.remove_option": "Fjern denne valgmulighed",
"compose_form.poll.remove_option": "Fjern valgmulighed",
"compose_form.poll.switch_to_multiple": "Ændr afstemning til flervalgstype",
"compose_form.poll.switch_to_single": "Ændr afstemning til enkeltvalgstype",
"compose_form.publish": "Toot",
"compose_form.publish": "Udgiv",
"compose_form.publish_loud": "{publish}!",
"compose_form.save_changes": "Save changes",
"compose_form.sensitive.hide": "{count, plural, one {Markér medie som følsomt} other {Markér medier som følsomme}}",
"compose_form.sensitive.marked": "{count, plural, one {Medie er markeret som sensitivt} other {Medier er markerede som sensitive}}",
"compose_form.sensitive.unmarked": "Intet medie market som sensitivt",
"compose_form.spoiler.marked": "Tekst skjult bag advarsel",
"compose_form.spoiler.unmarked": "Teksten er ikke skjult",
"compose_form.sensitive.unmarked": "{count, plural, one {Medie er ikke market som sensitivt} other {Medier er ikke markerede som sensitive}}",
"compose_form.spoiler.marked": "Fjern advarsel",
"compose_form.spoiler.unmarked": "Skjul bag advarsel",
"compose_form.spoiler_placeholder": "Skriv din advarsel hér",
"confirmation_modal.cancel": "Afbryd",
"confirmations.block.block_and_report": "Blokér og Anmeld",
"confirmations.block.confirm": "Blokér",
"confirmations.block.message": "Sikker på, at du vil blokere {name}?",
"confirmations.block.message": "Er du sikker på, du vil blokere {name}?",
"confirmations.delete.confirm": "Slet",
"confirmations.delete.message": "Sikker på, at du vil slette dette trut?",
"confirmations.delete.message": "Er du sikker på, at du vil slette indlægget?",
"confirmations.delete_list.confirm": "Slet",
"confirmations.delete_list.message": "Sikker på, at du vil slette denne liste permanent?",
"confirmations.discard_edit_media.confirm": "Discard",
"confirmations.discard_edit_media.message": "You have unsaved changes to the media description or preview, discard them anyway?",
"confirmations.domain_block.confirm": "Skjul hele domænet",
"confirmations.domain_block.message": "Helt sikker på, at du vil blokere hele {domain}-domænet? Oftest vil få, specifikke blokeringer eller tavsgørelser være nok og at fortrække. Du vil ikke se indhold fra domænet på offentlige tidslinjer eller i dine notifikationer. Dine følgere fra domænet fjernes.",
"confirmations.discard_edit_media.confirm": "Kassér",
"confirmations.discard_edit_media.message": "Der er ugemte ændringer i mediebeskrivelsen eller forhåndsvisningen, kassér dem alligevel?",
"confirmations.domain_block.confirm": "Blokér hele domænet",
"confirmations.domain_block.message": "Er du helt sikker på, at du vil blokere hele {domain}-domænet? Oftest vil det være at foretrække istedet målrettet at blokere eller skjule nogle få brugere. Hvis du blokerer domænet, vil du ikke se indhold fra domænet på offentlige tidslinjer eller i dine notifikationer. Dine følgere fra domænet fjernes.",
"confirmations.logout.confirm": "Log ud",
"confirmations.logout.message": "Log ud, sikker?",
"confirmations.mute.confirm": "Tavsgøre",
"confirmations.mute.explanation": "Dette skjuler indlæg fra dem (og om) dem, men det vil lade dem at se dine indlæg og følge dig.",
"confirmations.mute.message": "Sikker på, du vil tavsgøre {name}?",
"confirmations.mute.confirm": "Skjul",
"confirmations.mute.explanation": "Dette skjuler indlæg fra (og om) dem, men de kan stadig se dine indlæg og følge dig.",
"confirmations.mute.message": "Er du sikker på, du vil skjule {name}?",
"confirmations.redraft.confirm": "Slet og omskriv",
"confirmations.redraft.message": "Sikker på, at du vil slette dette trut og omskrive det? Favoritter og fremhævelser går tabt og svar til det oprindelige indlæg afassocieres.",
"confirmations.reply.confirm": "Besvar",
"confirmations.reply.message": "Besvarelse nu vil overskrive den besked, du er ved at skrive. Fortsæt alligevel?",
"confirmations.redraft.message": "Er du sikker på, at du vil slette og omskrive indlægget? Favoritter og fremhævelser går tabt og svar til det oprindelige indlæg afassocieres.",
"confirmations.reply.confirm": "Svar",
"confirmations.reply.message": "At svare nu vil overskrive den besked, du er ved at skrive. Fortsæt alligevel?",
"confirmations.unfollow.confirm": "Følg ikke længere",
"confirmations.unfollow.message": "Sikker på, at du ikke længere vil følge {name}?",
"conversation.delete": "Slet konversation",
"confirmations.unfollow.message": "Er du sikker på, at du ikke vil følge {name}?",
"conversation.delete": "Slet samtale",
"conversation.mark_as_read": "Markér som læst",
"conversation.open": "Vis konversation",
"conversation.open": "Vis samtale",
"conversation.with": "Med {names}",
"directory.federated": "Fra kendt fedivers",
"directory.federated": "Fra kendt Fællesvers",
"directory.local": "Kun fra {domain}",
"directory.new_arrivals": "Nye ankomster",
"directory.recently_active": "Senest aktiv",
"embed.instructions": "Indlejr dette trut på din side ved at kopiere koden nedenfor.",
"directory.recently_active": "Aktive for nyligt",
"embed.instructions": "Indlejr indlægget på din side ved at kopiere koden nedenfor.",
"embed.preview": "Sådan kommer det til at se ud:",
"emoji_button.activity": "Aktivitet",
"emoji_button.custom": "Tilpasset",
"emoji_button.flags": "Flag",
"emoji_button.food": "Mad og drikke",
"emoji_button.label": "Indsæt humørikon",
"emoji_button.label": "Indsæt emoji",
"emoji_button.nature": "Natur",
"emoji_button.not_found": "Ingen emojos!! (╯°□°)╯︵ ┻━┻",
"emoji_button.not_found": "Kunne ikke finde tilsvarende emojis",
"emoji_button.objects": "Objekter",
"emoji_button.people": "Personer",
"emoji_button.recent": "Oftest brugt",
"emoji_button.recent": "De sædvanlige",
"emoji_button.search": "Søg...",
"emoji_button.search_results": "Søgeresultater",
"emoji_button.symbols": "Symboler",
@ -160,32 +162,32 @@
"empty_column.account_suspended": "Konto suspenderet",
"empty_column.account_timeline": "Ingen trut her!",
"empty_column.account_unavailable": "Profil utilgængelig",
"empty_column.blocks": "Du har ikke blokeret nogle brugere endnu.",
"empty_column.bookmarked_statuses": "Du har ingen bogmærkede trut endnu. Når du bogmærker ét, vil det dukke op hér.",
"empty_column.blocks": "Du har ikke blokeret nogen endnu.",
"empty_column.bookmarked_statuses": "Du har ingen indlæg med bogmærke endnu. Når du sætter et bogmærke, vil det dukke op hér.",
"empty_column.community": "Den lokale tidslinje er tom. Skriv noget offentligt for at sætte tingene i gang!",
"empty_column.direct": "Du har endnu ingen direkte beskeder. Når du sender eller modtager en, vil den vises hér.",
"empty_column.domain_blocks": "Der er endnu ingen skjulte domæner.",
"empty_column.favourited_statuses": "Du har endnu ingen favorit-trut. Når du favoriserer ét, vil det blive vist hér.",
"empty_column.favourites": "Ingen har endnu favoriseret dette trut. Når nogen anden gør vil det blive vist hér.",
"empty_column.follow_recommendations": "Ser ud til, at der ikke kunne genereres forslag til dig. Du kan prøve med Søg for at lede efter personer, du måske kender, eller udforske hashtags.",
"empty_column.follow_requests": "Du har endnu ingen følgeranmodninger. Når du modtager én, vil den fremgå hér.",
"empty_column.hashtag": "Intet indhold i dette hashtag endnu.",
"empty_column.direct": "Du har ingen direkte beskeder endnu. Hvis du sender eller modtager en, bliver den vist hér.",
"empty_column.domain_blocks": "Der er ingen skjulte domæner endnu.",
"empty_column.favourited_statuses": "Du har ikke markeret nogle indlæg som favorit. Når du markerer ét, bliver det vist hér.",
"empty_column.favourites": "Ingen har markeret indlægget som favorit endnu. Hvis der er nogen der gør, bliver det vist hér.",
"empty_column.follow_recommendations": "Det ser ud til, at der ikke kunne blive lavet forslag til dig. Du kan prøve med Søg for at finde personer, du kender, eller udforske hashtags.",
"empty_column.follow_requests": "Du har ingen følgeanmodninger endnu. Hvis du modtager en, bliver den vist her.",
"empty_column.hashtag": "Der er ingen indlæg med hashtagget endnu.",
"empty_column.home": "Din hjemmetidslinje er tom! Besøg {public} eller brug søgningen for at komme igang og møde andre brugere.",
"empty_column.home.suggestions": "Se nogle foreslag",
"empty_column.list": "Der er endnu intet i denne liste. Når medlemmer af denne liste poster nye trut, vil de fremgå hér.",
"empty_column.list": "Der er ikke noget på denne liste endnu. Når medlemmer af listen udgiver nye indlæg vil de fremgå hér.",
"empty_column.lists": "Du har endnu ingen lister. Når du opretter én, vil den fremgå hér.",
"empty_column.mutes": "Du har endnu ikke tavsgjort nogle brugere.",
"empty_column.notifications": "Du har endnu ingen notifikationer. Interagér med andre for at starte konversationen.",
"empty_column.mutes": "Du har endnu ikke tystnet nogle brugere.",
"empty_column.notifications": "Du har ingen notifikationer. Hvis andre interagerer med dig, bliver det vist her.",
"empty_column.public": "Der er intet hér! Skriv noget offentligt eller følg manuelt brugere fra andre servere for at se indhold",
"error.unexpected_crash.explanation": "Grundet en fejl i vores kode, eller en browser-kompatibilitetsfejl, kunne siden ikke vises korrekt.",
"error.unexpected_crash.explanation_addons": "Denne side kunne ikke vises korrekt. Fejlen skyldes sandsynligvis en browsertilføjelse eller automatiske oversættelsesværktøjer.",
"error.unexpected_crash.next_steps": "Prøv at opfriske siden. Hjælper dette ikke, kan Mastodon muligvis stadig bruges via en anden browser eller app.",
"error.unexpected_crash.next_steps_addons": "Prøv at deaktivere dem og opfriske siden. Hjælper dette ikke, kan Mastodon muligvis stadig bruges via en anden browser eller app.",
"error.unexpected_crash.next_steps": "Prøv at genindlæs siden. Hvis dette ikke hjælper, så forsøg venligst, at tilgå Mastodon via en anden browser eller app.",
"error.unexpected_crash.next_steps_addons": "Prøv at deaktivere dem og genindlæse siden. Hvis det ikke hjælper, kan Mastodon muligvis stadig bruges via en anden browser eller app.",
"errors.unexpected_crash.copy_stacktrace": "Kopiér stacktrace til udklipsholderen",
"errors.unexpected_crash.report_issue": "Anmeld problem",
"follow_recommendations.done": "Udført",
"follow_recommendations.heading": "Følg personer du gerne vil se indlæg fra! Her er nogle forslag.",
"follow_recommendations.lead": "Indlæg, fra personer du følger, vises i kronologisk rækkefølge i dit hjemmefeed. Vær ikke bange for at begå fejl, du kan vælge \"følg ikke\" personer lige så nemt til enhver tid!",
"follow_recommendations.lead": "Indlæg, fra personer du følger, vises i kronologisk rækkefølge i hjemmetidslinjen. Bare prøv dig frem med hvem du følger her. Du kan altid vælge om igen!",
"follow_request.authorize": "Godkend",
"follow_request.reject": "Afvis",
"follow_requests.unlocked_explanation": "Selvom din konto ikke er låst, antog {domain}-personalet, at du måske vil gennemgå dine anmodninger manuelt.",
@ -193,9 +195,9 @@
"getting_started.developers": "Udviklere",
"getting_started.directory": "Profilliste",
"getting_started.documentation": "Dokumentation",
"getting_started.heading": "Komme i gang",
"getting_started.heading": "Startmenu",
"getting_started.invite": "Invitér folk",
"getting_started.open_source_notice": "Mastodon er en open-source software. Du kan bidrage eller anmelde fejl via GitHub {github}.",
"getting_started.open_source_notice": "Mastodon er open-source software. Du kan bidrage eller anmelde fejl via GitHub {github}.",
"getting_started.security": "Kontoindstillinger",
"getting_started.terms": "Tjenestevilkår",
"hashtag.column_header.tag_mode.all": "og {additional}",
@ -203,7 +205,7 @@
"hashtag.column_header.tag_mode.none": "uden {additional}",
"hashtag.column_settings.select.no_options_message": "Ingen forslag fundet",
"hashtag.column_settings.select.placeholder": "Angiv hashtags…",
"hashtag.column_settings.tag_mode.all": "Alle disse",
"hashtag.column_settings.tag_mode.all": "Allesammen",
"hashtag.column_settings.tag_mode.any": "Nogle af disse",
"hashtag.column_settings.tag_mode.none": "Ingen af disse",
"hashtag.column_settings.tag_toggle": "Inkludér ekstra tags for denne kolonne",
@ -216,17 +218,17 @@
"intervals.full.hours": "{number, plural, one {# time} other {# timer}}",
"intervals.full.minutes": "{number, plural, one {# minut} other {# minutter}}",
"keyboard_shortcuts.back": "for at navigere tilbage",
"keyboard_shortcuts.blocked": "for at åbne listen over blokerede brugere",
"keyboard_shortcuts.boost": "for at fremhæve",
"keyboard_shortcuts.blocked": "Vis listen over blokerede brugere",
"keyboard_shortcuts.boost": "Fremhæv indlæg",
"keyboard_shortcuts.column": "for at fokusere et trut i en af kolonnerne",
"keyboard_shortcuts.compose": "for at fokusere på skriveområdet",
"keyboard_shortcuts.description": "Beskrivelse",
"keyboard_shortcuts.direct": "for at åbne direkte besked-kolonnen",
"keyboard_shortcuts.direct": "Åben kolonnen med direkte beskeder",
"keyboard_shortcuts.down": "for at rykke nedad på listen",
"keyboard_shortcuts.enter": "for at åbne trut",
"keyboard_shortcuts.favourite": "for at favorisere",
"keyboard_shortcuts.enter": "Åben indlæg",
"keyboard_shortcuts.favourite": "Markér som favorit",
"keyboard_shortcuts.favourites": "for at åbne favoritlisten",
"keyboard_shortcuts.federated": "for at åbne den forenede tidslinje",
"keyboard_shortcuts.federated": "Åben den fælles tidslinje",
"keyboard_shortcuts.heading": "Tastaturgenveje",
"keyboard_shortcuts.home": "for at åbne hjemmetidslinjen",
"keyboard_shortcuts.hotkey": "Hurtigtast",
@ -237,10 +239,10 @@
"keyboard_shortcuts.my_profile": "for at åbne din profil",
"keyboard_shortcuts.notifications": "for at åbne notifikationskolonnen",
"keyboard_shortcuts.open_media": "for at åbne medier",
"keyboard_shortcuts.pinned": "for at åbne listen over fastgjorte trut",
"keyboard_shortcuts.pinned": "Åben listen over fastgjorte indlæg",
"keyboard_shortcuts.profile": "for at åbne forfatterens profil",
"keyboard_shortcuts.reply": "for at besvare",
"keyboard_shortcuts.requests": "for at åbne følganmodningslisten",
"keyboard_shortcuts.requests": "Åben listen over følgeanmodninger",
"keyboard_shortcuts.search": "for at fokusere søgningen",
"keyboard_shortcuts.spoilers": "for at vise/skjule CW-felt",
"keyboard_shortcuts.start": "for at åbne \"komme i gang\"-kolonnen",
@ -273,48 +275,48 @@
"missing_indicator.label": "Ikke fundet",
"missing_indicator.sublabel": "Denne ressource kunne ikke findes",
"mute_modal.duration": "Varighed",
"mute_modal.hide_notifications": "Skjul notifikationer fra denne bruger?",
"mute_modal.indefinite": "Uendeligt",
"navigation_bar.apps": "Mobil-apps",
"mute_modal.hide_notifications": "Skjul notifikationer fra brugeren?",
"mute_modal.indefinite": "Tidsubegrænset",
"navigation_bar.apps": "Apps til mobilen",
"navigation_bar.blocks": "Blokerede brugere",
"navigation_bar.bookmarks": "Bogmærker",
"navigation_bar.community_timeline": "Lokal tidslinje",
"navigation_bar.compose": "Skriv nyt trut",
"navigation_bar.compose": "Skriv nyt indlæg",
"navigation_bar.direct": "Direkte beskeder",
"navigation_bar.discover": "Opdag",
"navigation_bar.domain_blocks": "Blokerede domæner",
"navigation_bar.edit_profile": "Redigér profil",
"navigation_bar.favourites": "Favoritter",
"navigation_bar.filters": "Tavsgjorte ord",
"navigation_bar.follow_requests": "Følganmodninger",
"navigation_bar.follows_and_followers": "Følger og følgere",
"navigation_bar.filters": "Tystnede ord",
"navigation_bar.follow_requests": "Følgeanmodninger",
"navigation_bar.follows_and_followers": "Følges og følgere",
"navigation_bar.info": "Om denne server",
"navigation_bar.keyboard_shortcuts": "Hurtigtaster",
"navigation_bar.lists": "Lister",
"navigation_bar.logout": "Log ud",
"navigation_bar.mutes": "Tavsgjorte brugere",
"navigation_bar.logout": "Log af",
"navigation_bar.mutes": "Tystnede brugere",
"navigation_bar.personal": "Personlig",
"navigation_bar.pins": "Fastgjorte trut",
"navigation_bar.pins": "Fastgjorte indlæg",
"navigation_bar.preferences": "Præferencer",
"navigation_bar.public_timeline": "Forenet tidslinje",
"navigation_bar.public_timeline": "Fælles tidslinje",
"navigation_bar.security": "Sikkerhed",
"notification.favourite": "{name} favoriserede dit trut",
"notification.follow": "{name} fulgte dig",
"notification.follow_request": "{name} har anmodet om at følge dig",
"notification.mention": "{name} nævnte dig",
"notification.own_poll": "Din afstemning er afsluttet",
"notification.poll": "En afstemning, hvori du stemte, er slut",
"notification.reblog": "{name} fremhævede din trut",
"notification.status": "{name} har netop postet",
"notification.poll": "En afstemning, du deltog i, er færdig",
"notification.reblog": "{name} fremhævede dit indlæg",
"notification.status": "{name} har netop udgivet",
"notifications.clear": "Ryd notifikationer",
"notifications.clear_confirmation": "Sikker på, at du vil rydde alle dine notifikationer permanent?",
"notifications.clear_confirmation": "Er du sikker på, du vil rydde alle dine notifikationer permanent?",
"notifications.column_settings.alert": "Skrivebordsnotifikationer",
"notifications.column_settings.favourite": "Favoritter:",
"notifications.column_settings.filter_bar.advanced": "Vis alle kategorier",
"notifications.column_settings.filter_bar.category": "Hurtigfilterbjælke",
"notifications.column_settings.filter_bar.show": "Vis",
"notifications.column_settings.filter_bar.show_bar": "Vis filterbjælke",
"notifications.column_settings.follow": "Nye følgere:",
"notifications.column_settings.follow_request": "Nye følganmodninger:",
"notifications.column_settings.follow_request": "Nye følgeanmodninger:",
"notifications.column_settings.mention": "Omtaler:",
"notifications.column_settings.poll": "Afstemningsresultater:",
"notifications.column_settings.push": "Pushnotifikationer",
@ -322,7 +324,8 @@
"notifications.column_settings.show": "Vis i kolonne",
"notifications.column_settings.sound": "Afspil lyd",
"notifications.column_settings.status": "Nye indlæg:",
"notifications.column_settings.unread_markers.category": "Ulæste notifkationer-markører",
"notifications.column_settings.unread_notifications.category": "Ulæste notifikationer",
"notifications.column_settings.unread_notifications.highlight": "Fremhæv ulæste notifikationer",
"notifications.filter.all": "Alle",
"notifications.filter.boosts": "Fremhævelser",
"notifications.filter.favourites": "Favoritter",
@ -341,36 +344,44 @@
"notifications_permission_banner.title": "Gå aldrig glip af noget",
"picture_in_picture.restore": "Sæt det tilbage",
"poll.closed": "Lukket",
"poll.refresh": "Opfrisk",
"poll.refresh": "Opdatér",
"poll.total_people": "{count, plural, one {# person} other {# personer}}",
"poll.total_votes": "{count, plural, one {# stemme} other {# stemmer}}",
"poll.vote": "Stem",
"poll.voted": "Du stemte for dette svar",
"poll.votes": "{votes, plural, one {# vote} other {# votes}}",
"poll.voted": "Du stemte dette svar",
"poll.votes": "{votes, plural, one {# stemme} other {# stemmer}}",
"poll_button.add_poll": "Tilføj en afstemning",
"poll_button.remove_poll": "Fjern afstemning",
"privacy.change": "Justér trutfortrolighed",
"privacy.change": "Justér fortrolighed",
"privacy.direct.long": "Kun synlig for nævnte brugere",
"privacy.direct.short": "Direkte",
"privacy.private.long": "Kun synlig for følgere",
"privacy.private.short": "Kun for følgere",
"privacy.public.long": "Synlig for alle på offentlige tidslinjer",
"privacy.public.long": "Synlig for alle og vises på offentlige tidslinjer",
"privacy.public.short": "Offentlig",
"privacy.unlisted.long": "Synlig for alle, men på offentlige tidslinjer",
"privacy.unlisted.short": "Ulistet",
"refresh": "Opfrisk",
"privacy.unlisted.long": "Synlig for alle, men vises ikke på offentlige tidslinjer",
"privacy.unlisted.short": "Diskret",
"refresh": "Genindlæs",
"regeneration_indicator.label": "Indlæser…",
"regeneration_indicator.sublabel": "Din hjemmefeed klargøres!",
"regeneration_indicator.sublabel": "Din hjemmetidslinje klargøres!",
"relative_time.days": "{number}d",
"relative_time.full.days": "{number, plural, one {# day} other {# days}} ago",
"relative_time.full.hours": "{number, plural, one {# hour} other {# hours}} ago",
"relative_time.full.just_now": "just now",
"relative_time.full.minutes": "{number, plural, one {# minute} other {# minutes}} ago",
"relative_time.full.seconds": "{number, plural, one {# second} other {# seconds}} ago",
"relative_time.hours": "{number}t",
"relative_time.just_now": "nu",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"relative_time.today": "i dag",
"reply_indicator.cancel": "Afbryd",
"report.categories.other": "Other",
"report.categories.spam": "Spam",
"report.categories.violation": "Content violates one or more server rules",
"report.forward": "Videresend til {target}",
"report.forward_hint": "Kontoen er fra en anden server. Sende en anonymiseret anmeldelseskopi dertil også?",
"report.hint": "Anmeldelsen sendes til din serverordstyrerer. Du kan oplyse nærmere om kontoanmeldelsen nedennfor:",
"report.forward_hint": "Kontoen er fra en anden server. Send en anonymiseret kopi af anmeldelsen dertil også?",
"report.hint": "Anmeldelsen sendes til din serverordstyrer. Du kan oplyse nærmere om kontoanmeldelsen nedenfor:",
"report.placeholder": "Yderligere kommentarer",
"report.submit": "Indsend",
"report.target": "Anmelder {target}",
@ -378,47 +389,52 @@
"search_popout.search_format": "Avanceret søgeformat",
"search_popout.tips.full_text": "Simpel tekst returnerer trut, du har skrevet, favoriseret, fremhævede eller som er nævnt i/matcher bruger- og profilnavne samt hashtags.",
"search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "trut",
"search_popout.tips.status": "indlæg",
"search_popout.tips.text": "Simpel tekst returnerer matchende visnings- og brugernavne samt hashtags",
"search_popout.tips.user": "bruger",
"search_results.accounts": "Personer",
"search_results.hashtags": "Hashtags",
"search_results.statuses": "Trut",
"search_results.statuses": "Indlæg",
"search_results.statuses_fts_disabled": "På denne Mastodon-server er trutsøgning efter deres indhold ikke aktiveret.",
"search_results.total": "{count, number} {count, plural, one {resultat} other {resultater}}",
"status.admin_account": "Åbn modereringsbrugerflade for @{name}",
"status.admin_status": "Åbn dette trut i modereringsbrugerflade",
"status.block": "Blokér @{name}",
"status.bookmark": "Bogmærke",
"status.bookmark": "Tilføj bogmærke",
"status.cancel_reblog_private": "Fjern fremhævning",
"status.cannot_reblog": "Dette indlæg kan ikke fremhæves",
"status.cannot_reblog": "Indlægget kan ikke fremhæves",
"status.copy": "Kopiér link til trut",
"status.delete": "Slet",
"status.detailed_status": "Detaljeret konversationsvisning",
"status.detailed_status": "Detaljeret samtalevisning",
"status.direct": "Direkte besked til @{name}",
"status.edit": "Edit",
"status.edited": "Edited {date}",
"status.edited_x_times": "Edited {count, plural, one {{count} time} other {{count} times}}",
"status.embed": "Indlejr",
"status.favourite": "Favorit",
"status.filtered": "Filtreret",
"status.history.created": "{name} created {date}",
"status.history.edited": "{name} edited {date}",
"status.load_more": "Indlæs mere",
"status.media_hidden": "Medie skjult",
"status.mention": "Nævn @{name}",
"status.more": "Mere",
"status.mute": "Tavsgør @{name}",
"status.mute_conversation": "Tavsgør konversation",
"status.open": "Udvid dette trut",
"status.mute": "Skjul @{name}",
"status.mute_conversation": "Skjul samtale",
"status.open": "Udvid indlægget",
"status.pin": "Fastgør til profil",
"status.pinned": "Fastgjort trut",
"status.read_more": "Læs mere",
"status.reblog": "Fremhæv",
"status.reblog_private": "Fremhæv med oprindelig synlighed",
"status.reblogged_by": "{name} fremhævet",
"status.reblogs.empty": "Ingen har endnu fremhævet dette trut. Når nogen gør, vil det fremgå hér.",
"status.reblogs.empty": "Ingen har fremhævet indlægget endnu. Når nogen gør, vil det fremgå hér.",
"status.redraft": "Slet og omskriv",
"status.remove_bookmark": "Fjern bogmærke",
"status.reply": "Besvar",
"status.replyAll": "Besvar til tråd",
"status.replyAll": "Svar på tråd",
"status.report": "Anmeld @{name}",
"status.sensitive_warning": "Sensitivt indhold",
"status.sensitive_warning": "Følsomt indhold",
"status.share": "Del",
"status.show_less": "Vis mindre",
"status.show_less_all": "Vis mindre for alle",
@ -426,11 +442,11 @@
"status.show_more_all": "Vis mere for alle",
"status.show_thread": "Vis tråd",
"status.uncached_media_warning": "Utilgængelig",
"status.unmute_conversation": "Genaktivér konversation",
"status.unmute_conversation": "Genaktivér samtale",
"status.unpin": "Frigør fra profil",
"suggestions.dismiss": "Afvis foreslag",
"suggestions.header": "Du er måske interesseret i…",
"tabs_bar.federated_timeline": "Forenede",
"tabs_bar.federated_timeline": "Fælles",
"tabs_bar.home": "Hjem",
"tabs_bar.local_timeline": "Lokal",
"tabs_bar.notifications": "Notifikationer",
@ -438,7 +454,7 @@
"time_remaining.days": "{number, plural, one {# dag} other {# dage}} tilbage",
"time_remaining.hours": "{number, plural, one {# time} other {# timer}} tilbage",
"time_remaining.minutes": "{number, plural, one {# minut} other {# minutter}} tilbage",
"time_remaining.moments": "Få øjeblikke tilbage",
"time_remaining.moments": "Et øjeblik tilbage",
"time_remaining.seconds": "{number, plural, one {# sekund} other {# sekunder}} tilbage",
"timeline_hint.remote_resource_not_displayed": "{resource} fra andre servere vises ikke.",
"timeline_hint.resources.followers": "Følgere",
@ -446,13 +462,13 @@
"timeline_hint.resources.statuses": "Ældre indlæg",
"trends.counter_by_accounts": "{count, plural, one {{counter} person} other {{counter} personer}} taler",
"trends.trending_now": "Hot lige nu",
"ui.beforeunload": "Dit udkast går tabt, hvis du forlader Mastodon.",
"units.short.billion": "{count}MI",
"ui.beforeunload": "Dit udkast går tabt, hvis du lukker Mastodon.",
"units.short.billion": "{count}G",
"units.short.million": "{count}M",
"units.short.thousand": "{count}K",
"units.short.thousand": "{count}k",
"upload_area.title": "Træk og slip for at uploade",
"upload_button.label": "Tilføj billeder, en video- eller lydfil",
"upload_error.limit": "Filuploadgrænse nået.",
"upload_error.limit": "Grænse for filupload nået.",
"upload_error.poll": "Filupload ikke tilladt for afstemninger.",
"upload_form.audio_description": "Beskrivelse til hørehæmmede",
"upload_form.description": "Beskrivelse til svagtseende",
@ -462,9 +478,9 @@
"upload_form.video_description": "Beskrivelse for hørehæmmede eller synshandicappede personer",
"upload_modal.analyzing_picture": "Analyserer billede…",
"upload_modal.apply": "Anvend",
"upload_modal.applying": "Applying…",
"upload_modal.applying": "Effektuerer…",
"upload_modal.choose_image": "Vælg billede",
"upload_modal.description_placeholder": "En hurtig brun ræv hopper over den dovne hund",
"upload_modal.description_placeholder": "Høj bly gom vandt fræk sexquiz på wc",
"upload_modal.detect_text": "Detektér tekst i billede",
"upload_modal.edit_media": "Redigér medie",
"upload_modal.hint": "Klik eller træk cirklen i forhåndsvisningen for at vælge det fokuspunkt, der altid vil være synligt på alle miniaturer.",
@ -477,8 +493,8 @@
"video.expand": "Udvid video",
"video.fullscreen": "Fuldskærm",
"video.hide": "Skjul video",
"video.mute": "Tavsgør lyd",
"video.mute": "Sluk lyden",
"video.pause": "Sæt på pause",
"video.play": "Afspil",
"video.unmute": "Fjern lydtavsgørelse"
"video.unmute": "Tænd for lyden"
}

View File

@ -47,16 +47,17 @@
"account.unmute": "@{name} nicht mehr stummschalten",
"account.unmute_notifications": "Benachrichtigungen von @{name} einschalten",
"account_note.placeholder": "Notiz durch Klicken hinzufügen",
"admin.dashboard.retention": "Retention",
"admin.dashboard.retention.average": "Average",
"admin.dashboard.retention.cohort": "Sign-up month",
"admin.dashboard.retention.cohort_size": "New users",
"admin.dashboard.daily_retention": "User retention rate by day after sign-up",
"admin.dashboard.monthly_retention": "User retention rate by month after sign-up",
"admin.dashboard.retention.average": "Durchschnitt",
"admin.dashboard.retention.cohort": "Anmeldemonat",
"admin.dashboard.retention.cohort_size": "Neue Benutzer",
"alert.rate_limited.message": "Bitte versuche es nach {retry_time, time, medium}.",
"alert.rate_limited.title": "Anfragelimit überschritten",
"alert.unexpected.message": "Ein unerwarteter Fehler ist aufgetreten.",
"alert.unexpected.title": "Hoppla!",
"announcement.announcement": "Ankündigung",
"attachments_list.unprocessed": "(unprocessed)",
"attachments_list.unprocessed": "(unverarbeitet)",
"autosuggest_hashtag.per_week": "{count} pro Woche",
"boost_modal.combo": "Drücke {combo}, um dieses Fenster zu überspringen",
"bundle_column_error.body": "Etwas ist beim Laden schiefgelaufen.",
@ -104,6 +105,7 @@
"compose_form.poll.switch_to_single": "Umfrage ändern, um eine einzige Wahl zu erlauben",
"compose_form.publish": "Tröt",
"compose_form.publish_loud": "{publish}!",
"compose_form.save_changes": "Save changes",
"compose_form.sensitive.hide": "Medien als NSFW markieren",
"compose_form.sensitive.marked": "Medien sind als NSFW markiert",
"compose_form.sensitive.unmarked": "Medien sind nicht als NSFW markiert",
@ -118,8 +120,8 @@
"confirmations.delete.message": "Bist du dir sicher, dass du diesen Beitrag löschen möchtest?",
"confirmations.delete_list.confirm": "Löschen",
"confirmations.delete_list.message": "Bist du dir sicher, dass du diese Liste permanent löschen möchtest?",
"confirmations.discard_edit_media.confirm": "Discard",
"confirmations.discard_edit_media.message": "You have unsaved changes to the media description or preview, discard them anyway?",
"confirmations.discard_edit_media.confirm": "Verwerfen",
"confirmations.discard_edit_media.message": "Du hast ungespeicherte Änderungen an der Medienbeschreibung oder der Medienvorschau. Trotzdem verwerfen?",
"confirmations.domain_block.confirm": "Die ganze Domain blockieren",
"confirmations.domain_block.message": "Bist du dir wirklich sicher, dass du die ganze Domain {domain} blockieren willst? In den meisten Fällen reichen ein paar gezielte Blockierungen oder Stummschaltungen aus. Du wirst den Inhalt von dieser Domain nicht in irgendwelchen öffentlichen Timelines oder den Benachrichtigungen finden. Deine Folgenden von dieser Domain werden entfernt.",
"confirmations.logout.confirm": "Abmelden",
@ -312,7 +314,7 @@
"notifications.column_settings.favourite": "Favorisierungen:",
"notifications.column_settings.filter_bar.advanced": "Zeige alle Kategorien an",
"notifications.column_settings.filter_bar.category": "Schnellfilterleiste",
"notifications.column_settings.filter_bar.show": "Anzeigen",
"notifications.column_settings.filter_bar.show_bar": "Filterleiste anzeigen",
"notifications.column_settings.follow": "Neue Folgende:",
"notifications.column_settings.follow_request": "Neue Folgeanfragen:",
"notifications.column_settings.mention": "Erwähnungen:",
@ -322,7 +324,8 @@
"notifications.column_settings.show": "In der Spalte anzeigen",
"notifications.column_settings.sound": "Ton abspielen",
"notifications.column_settings.status": "Neue Beiträge:",
"notifications.column_settings.unread_markers.category": "Ungelesene Benachrichtigungsmarkierungen",
"notifications.column_settings.unread_notifications.category": "Ungelesene Benachrichtigungen",
"notifications.column_settings.unread_notifications.highlight": "Ungelesene Benachrichtigungen hervorheben",
"notifications.filter.all": "Alle",
"notifications.filter.boosts": "Geteilte Beiträge",
"notifications.filter.favourites": "Favorisierungen",
@ -346,7 +349,7 @@
"poll.total_votes": "{count, plural, one {# Stimme} other {# Stimmen}}",
"poll.vote": "Abstimmen",
"poll.voted": "Du hast dafür gestimmt",
"poll.votes": "{votes, plural, one {# vote} other {# votes}}",
"poll.votes": "{votes, plural, one {# Stimme} other {# Stimmen}}",
"poll_button.add_poll": "Eine Umfrage erstellen",
"poll_button.remove_poll": "Umfrage entfernen",
"privacy.change": "Sichtbarkeit des Beitrags anpassen",
@ -362,12 +365,20 @@
"regeneration_indicator.label": "Laden…",
"regeneration_indicator.sublabel": "Deine Startseite wird gerade vorbereitet!",
"relative_time.days": "{number}d",
"relative_time.full.days": "{number, plural, one {# day} other {# days}} ago",
"relative_time.full.hours": "{number, plural, one {# hour} other {# hours}} ago",
"relative_time.full.just_now": "just now",
"relative_time.full.minutes": "{number, plural, one {# minute} other {# minutes}} ago",
"relative_time.full.seconds": "{number, plural, one {# second} other {# seconds}} ago",
"relative_time.hours": "{number}h",
"relative_time.just_now": "jetzt",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"relative_time.today": "heute",
"reply_indicator.cancel": "Abbrechen",
"report.categories.other": "Other",
"report.categories.spam": "Spam",
"report.categories.violation": "Content violates one or more server rules",
"report.forward": "An {target} weiterleiten",
"report.forward_hint": "Dieses Konto ist von einem anderen Server. Soll eine anonymisierte Kopie des Berichts auch dorthin geschickt werden?",
"report.hint": "Der Bericht wird an die Moderatoren des Servers geschickt. Du kannst hier eine Erklärung angeben, warum du dieses Konto meldest:",
@ -396,9 +407,14 @@
"status.delete": "Löschen",
"status.detailed_status": "Detaillierte Ansicht der Konversation",
"status.direct": "Direktnachricht @{name}",
"status.edit": "Edit",
"status.edited": "Edited {date}",
"status.edited_x_times": "Edited {count, plural, one {{count} time} other {{count} times}}",
"status.embed": "Einbetten",
"status.favourite": "Favorisieren",
"status.filtered": "Gefiltert",
"status.history.created": "{name} created {date}",
"status.history.edited": "{name} edited {date}",
"status.load_more": "Weitere laden",
"status.media_hidden": "Medien versteckt",
"status.mention": "@{name} erwähnen",
@ -462,7 +478,7 @@
"upload_form.video_description": "Beschreibe das Video für Menschen mit einer Hör- oder Sehbehinderung",
"upload_modal.analyzing_picture": "Analysiere Bild…",
"upload_modal.apply": "Übernehmen",
"upload_modal.applying": "Applying…",
"upload_modal.applying": "Anwenden…",
"upload_modal.choose_image": "Bild auswählen",
"upload_modal.description_placeholder": "Die heiße Zypernsonne quälte Max und Victoria ja böse auf dem Weg bis zur Küste",
"upload_modal.detect_text": "Text aus Bild erkennen",

View File

@ -79,6 +79,23 @@
],
"path": "app/javascript/mastodon/components/account.json"
},
{
"descriptors": [
{
"defaultMessage": "Other",
"id": "report.categories.other"
},
{
"defaultMessage": "Spam",
"id": "report.categories.spam"
},
{
"defaultMessage": "Content violates one or more server rules",
"id": "report.categories.violation"
}
],
"path": "app/javascript/mastodon/components/admin/ReportReasonSelector.json"
},
{
"descriptors": [
{
@ -98,8 +115,12 @@
"id": "admin.dashboard.retention.average"
},
{
"defaultMessage": "Retention",
"id": "admin.dashboard.retention"
"defaultMessage": "User retention rate by day after sign-up",
"id": "admin.dashboard.daily_retention"
},
{
"defaultMessage": "User retention rate by month after sign-up",
"id": "admin.dashboard.monthly_retention"
}
],
"path": "app/javascript/mastodon/components/admin/Retention.json"
@ -208,6 +229,27 @@
],
"path": "app/javascript/mastodon/components/domain.json"
},
{
"descriptors": [
{
"defaultMessage": "Edited {count, plural, one {{count} time} other {{count} times}}",
"id": "status.edited_x_times"
},
{
"defaultMessage": "{name} created {date}",
"id": "status.history.created"
},
{
"defaultMessage": "{name} edited {date}",
"id": "status.history.edited"
},
{
"defaultMessage": "Edited {date}",
"id": "status.edited"
}
],
"path": "app/javascript/mastodon/components/edited_timestamp/index.json"
},
{
"descriptors": [
{
@ -381,22 +423,42 @@
"defaultMessage": "now",
"id": "relative_time.just_now"
},
{
"defaultMessage": "just now",
"id": "relative_time.full.just_now"
},
{
"defaultMessage": "{number}s",
"id": "relative_time.seconds"
},
{
"defaultMessage": "{number, plural, one {# second} other {# seconds}} ago",
"id": "relative_time.full.seconds"
},
{
"defaultMessage": "{number}m",
"id": "relative_time.minutes"
},
{
"defaultMessage": "{number, plural, one {# minute} other {# minutes}} ago",
"id": "relative_time.full.minutes"
},
{
"defaultMessage": "{number}h",
"id": "relative_time.hours"
},
{
"defaultMessage": "{number, plural, one {# hour} other {# hours}} ago",
"id": "relative_time.full.hours"
},
{
"defaultMessage": "{number}d",
"id": "relative_time.days"
},
{
"defaultMessage": "{number, plural, one {# day} other {# days}} ago",
"id": "relative_time.full.days"
},
{
"defaultMessage": "Moments remaining",
"id": "time_remaining.moments"
@ -447,6 +509,10 @@
"defaultMessage": "Delete & re-draft",
"id": "status.redraft"
},
{
"defaultMessage": "Edit",
"id": "status.edit"
},
{
"defaultMessage": "Direct message @{name}",
"id": "status.direct"
@ -605,6 +671,10 @@
"defaultMessage": "Direct",
"id": "privacy.direct.short"
},
{
"defaultMessage": "Edited {date}",
"id": "status.edited"
},
{
"defaultMessage": "Filtered",
"id": "status.filtered"
@ -1106,6 +1176,10 @@
{
"defaultMessage": "{publish}!",
"id": "compose_form.publish_loud"
},
{
"defaultMessage": "Save changes",
"id": "compose_form.save_changes"
}
],
"path": "app/javascript/mastodon/features/compose/components/compose_form.json"
@ -2593,6 +2667,10 @@
"defaultMessage": "Delete & re-draft",
"id": "status.redraft"
},
{
"defaultMessage": "Edit",
"id": "status.edit"
},
{
"defaultMessage": "Direct message @{name}",
"id": "status.direct"
@ -2918,6 +2996,19 @@
],
"path": "app/javascript/mastodon/features/ui/components/columns_area.json"
},
{
"descriptors": [
{
"defaultMessage": "{name} created {date}",
"id": "status.history.created"
},
{
"defaultMessage": "{name} edited {date}",
"id": "status.history.edited"
}
],
"path": "app/javascript/mastodon/features/ui/components/compare_history_modal.json"
},
{
"descriptors": [
{

View File

@ -47,16 +47,17 @@
"account.unmute": "Διακοπή αποσιώπησης @{name}",
"account.unmute_notifications": "Διακοπή αποσιώπησης ειδοποιήσεων του/της @{name}",
"account_note.placeholder": "Κλικ για να βάλεις σημείωση",
"admin.dashboard.retention": "Retention",
"admin.dashboard.daily_retention": "User retention rate by day after sign-up",
"admin.dashboard.monthly_retention": "User retention rate by month after sign-up",
"admin.dashboard.retention.average": "Average",
"admin.dashboard.retention.cohort": "Sign-up month",
"admin.dashboard.retention.cohort_size": "New users",
"admin.dashboard.retention.cohort": "Μήνας εγγραφής",
"admin.dashboard.retention.cohort_size": "Νέοι χρήστες",
"alert.rate_limited.message": "Παρακαλούμε δοκίμασε ξανά αφού περάσει η {retry_time, time, medium}.",
"alert.rate_limited.title": "Περιορισμός συχνότητας",
"alert.unexpected.message": "Προέκυψε απροσδόκητο σφάλμα.",
"alert.unexpected.title": "Εεπ!",
"announcement.announcement": "Ανακοίνωση",
"attachments_list.unprocessed": "(unprocessed)",
"attachments_list.unprocessed": "(μη επεξεργασμένο)",
"autosuggest_hashtag.per_week": "{count} ανα εβδομάδα",
"boost_modal.combo": "Μπορείς να πατήσεις {combo} για να το προσπεράσεις αυτό την επόμενη φορά",
"bundle_column_error.body": "Κάτι πήγε στραβά ενώ φορτωνόταν αυτό το στοιχείο.",
@ -104,6 +105,7 @@
"compose_form.poll.switch_to_single": "Ενημέρωση δημοσκόπησης με μοναδική επιλογή",
"compose_form.publish": "Τουτ",
"compose_form.publish_loud": "{publish}!",
"compose_form.save_changes": "Save changes",
"compose_form.sensitive.hide": "Σημείωσε τα πολυμέσα ως ευαίσθητα",
"compose_form.sensitive.marked": "Το πολυμέσο έχει σημειωθεί ως ευαίσθητο",
"compose_form.sensitive.unmarked": "Το πολυμέσο δεν έχει σημειωθεί ως ευαίσθητο",
@ -118,8 +120,8 @@
"confirmations.delete.message": "Σίγουρα θες να διαγράψεις αυτή τη δημοσίευση;",
"confirmations.delete_list.confirm": "Διέγραψε",
"confirmations.delete_list.message": "Σίγουρα θες να διαγράψεις οριστικά αυτή τη λίστα;",
"confirmations.discard_edit_media.confirm": "Discard",
"confirmations.discard_edit_media.message": "You have unsaved changes to the media description or preview, discard them anyway?",
"confirmations.discard_edit_media.confirm": "Απόρριψη",
"confirmations.discard_edit_media.message": "Έχετε μη αποθηκευμένες αλλαγές στην περιγραφή πολυμέσων ή στην προεπισκόπηση, απορρίψτε τις ούτως ή άλλως;",
"confirmations.domain_block.confirm": "Απόκρυψη ολόκληρου του τομέα",
"confirmations.domain_block.message": "Σίγουρα θες να μπλοκάρεις ολόκληρο το {domain}; Συνήθως μερικά εστιασμένα μπλοκ ή αποσιωπήσεις επαρκούν και προτιμούνται. Δεν θα βλέπεις περιεχόμενο από αυτό τον κόμβο σε καμία δημόσια ροή, ούτε στις ειδοποιήσεις σου. Όσους ακόλουθους έχεις αυτό αυτό τον κόμβο θα αφαιρεθούν.",
"confirmations.logout.confirm": "Αποσύνδεση",
@ -312,7 +314,7 @@
"notifications.column_settings.favourite": "Αγαπημένα:",
"notifications.column_settings.filter_bar.advanced": "Εμφάνιση όλων των κατηγοριών",
"notifications.column_settings.filter_bar.category": "Μπάρα γρήγορου φίλτρου",
"notifications.column_settings.filter_bar.show": "Εμφάνιση",
"notifications.column_settings.filter_bar.show_bar": "Εμφάνιση μπάρας φίλτρου",
"notifications.column_settings.follow": "Νέοι ακόλουθοι:",
"notifications.column_settings.follow_request": "Νέο αίτημα παρακολούθησης:",
"notifications.column_settings.mention": "Αναφορές:",
@ -322,7 +324,8 @@
"notifications.column_settings.show": "Εμφάνισε σε στήλη",
"notifications.column_settings.sound": "Ηχητική ειδοποίηση",
"notifications.column_settings.status": "Νέα τουτ:",
"notifications.column_settings.unread_markers.category": "Δείκτες μη αναγνωσμένων ειδοποιήσεων",
"notifications.column_settings.unread_notifications.category": "Μη αναγνωσμένες ειδοποιήσεις",
"notifications.column_settings.unread_notifications.highlight": "Επισήμανση μη αναγνωσμένων ειδοποιήσεων",
"notifications.filter.all": "Όλες",
"notifications.filter.boosts": "Προωθήσεις",
"notifications.filter.favourites": "Αγαπημένα",
@ -346,7 +349,7 @@
"poll.total_votes": "{count, plural, one {# ψήφος} other {# ψήφοι}}",
"poll.vote": "Ψήφισε",
"poll.voted": "Ψηφίσατε αυτήν την απάντηση",
"poll.votes": "{votes, plural, one {# vote} other {# votes}}",
"poll.votes": "{votes, plural, one {# ψήφος} other {# ψήφοι}}",
"poll_button.add_poll": "Προσθήκη δημοσκόπησης",
"poll_button.remove_poll": "Αφαίρεση δημοσκόπησης",
"privacy.change": "Προσαρμογή ιδιωτικότητας δημοσίευσης",
@ -362,12 +365,20 @@
"regeneration_indicator.label": "Φορτώνει…",
"regeneration_indicator.sublabel": "Η αρχική σου ροή ετοιμάζεται!",
"relative_time.days": "{number}η",
"relative_time.full.days": "{number, plural, one {# day} other {# days}} ago",
"relative_time.full.hours": "{number, plural, one {# hour} other {# hours}} ago",
"relative_time.full.just_now": "just now",
"relative_time.full.minutes": "{number, plural, one {# minute} other {# minutes}} ago",
"relative_time.full.seconds": "{number, plural, one {# second} other {# seconds}} ago",
"relative_time.hours": "{number}ω",
"relative_time.just_now": "τώρα",
"relative_time.minutes": "{number}λ",
"relative_time.seconds": "{number}δ",
"relative_time.today": "σήμερα",
"reply_indicator.cancel": "Άκυρο",
"report.categories.other": "Other",
"report.categories.spam": "Spam",
"report.categories.violation": "Content violates one or more server rules",
"report.forward": "Προώθηση προς {target}",
"report.forward_hint": "Ο λογαριασμός είναι από διαφορετικό διακομιστή. Να σταλεί ανώνυμο αντίγραφο της καταγγελίας κι εκεί;",
"report.hint": "Η καταγγελία θα σταλεί στους διαχειριστές του κόμβου σου. Μπορείς να περιγράψεις γιατί καταγγέλεις αυτόν το λογαριασμό παρακάτω:",
@ -396,9 +407,14 @@
"status.delete": "Διαγραφή",
"status.detailed_status": "Προβολή λεπτομερειών συζήτησης",
"status.direct": "Προσωπικό μήνυμα προς @{name}",
"status.edit": "Edit",
"status.edited": "Edited {date}",
"status.edited_x_times": "Edited {count, plural, one {{count} time} other {{count} times}}",
"status.embed": "Ενσωμάτωσε",
"status.favourite": "Σημείωσε ως αγαπημένο",
"status.filtered": "Φιλτραρισμένα",
"status.history.created": "{name} created {date}",
"status.history.edited": "{name} edited {date}",
"status.load_more": "Φόρτωσε περισσότερα",
"status.media_hidden": "Κρυμμένο πολυμέσο",
"status.mention": "Ανέφερε τον/την @{name}",
@ -462,7 +478,7 @@
"upload_form.video_description": "Περιγραφή για άτομα με προβλήματα ακοής ή όρασης",
"upload_modal.analyzing_picture": "Ανάλυση εικόνας…",
"upload_modal.apply": "Εφαρμογή",
"upload_modal.applying": "Applying…",
"upload_modal.applying": "Εφαρμογή…",
"upload_modal.choose_image": "Επιλογή εικόνας",
"upload_modal.description_placeholder": "Λύκος μαύρος και ισχνός του πατέρα του καημός",
"upload_modal.detect_text": "Αναγνώριση κειμένου από την εικόνα",

View File

@ -47,7 +47,8 @@
"account.unmute": "Unmute @{name}",
"account.unmute_notifications": "Unmute notifications from @{name}",
"account_note.placeholder": "Click to add note",
"admin.dashboard.retention": "Retention",
"admin.dashboard.daily_retention": "User retention rate by day after sign-up",
"admin.dashboard.monthly_retention": "User retention rate by month after sign-up",
"admin.dashboard.retention.average": "Average",
"admin.dashboard.retention.cohort": "Sign-up month",
"admin.dashboard.retention.cohort_size": "New users",
@ -108,6 +109,7 @@
"compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
"compose_form.publish": "Toot",
"compose_form.publish_loud": "{publish}!",
"compose_form.save_changes": "Save changes",
"compose_form.sensitive.hide": "{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}",
"compose_form.sensitive.marked": "{count, plural, one {Media is marked as sensitive} other {Media is marked as sensitive}}",
"compose_form.sensitive.unmarked": "{count, plural, one {Media is not marked as sensitive} other {Media is not marked as sensitive}}",
@ -368,12 +370,20 @@
"regeneration_indicator.label": "Loading…",
"regeneration_indicator.sublabel": "Your home feed is being prepared!",
"relative_time.days": "{number}d",
"relative_time.full.days": "{number, plural, one {# day} other {# days}} ago",
"relative_time.full.hours": "{number, plural, one {# hour} other {# hours}} ago",
"relative_time.full.just_now": "just now",
"relative_time.full.minutes": "{number, plural, one {# minute} other {# minutes}} ago",
"relative_time.full.seconds": "{number, plural, one {# second} other {# seconds}} ago",
"relative_time.hours": "{number}h",
"relative_time.just_now": "now",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"relative_time.today": "today",
"reply_indicator.cancel": "Cancel",
"report.categories.other": "Other",
"report.categories.spam": "Spam",
"report.categories.violation": "Content violates one or more server rules",
"report.forward": "Forward to {target}",
"report.forward_hint": "The account is from another server. Send an anonymized copy of the report there as well?",
"report.hint": "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:",
@ -402,9 +412,14 @@
"status.delete": "Delete",
"status.detailed_status": "Detailed conversation view",
"status.direct": "Direct message @{name}",
"status.edit": "Edit",
"status.edited": "Edited {date}",
"status.edited_x_times": "Edited {count, plural, one {{count} time} other {{count} times}}",
"status.embed": "Embed",
"status.favourite": "Favourite",
"status.filtered": "Filtered",
"status.history.created": "{name} created {date}",
"status.history.edited": "{name} edited {date}",
"status.load_more": "Load more",
"status.media_hidden": "Media hidden",
"status.mention": "Mention @{name}",

View File

@ -47,16 +47,17 @@
"account.unmute": "Malsilentigi @{name}",
"account.unmute_notifications": "Malsilentigi sciigojn de @{name}",
"account_note.placeholder": "Alklaku por aldoni noton",
"admin.dashboard.retention": "Retention",
"admin.dashboard.retention.average": "Average",
"admin.dashboard.retention.cohort": "Sign-up month",
"admin.dashboard.retention.cohort_size": "New users",
"admin.dashboard.daily_retention": "User retention rate by day after sign-up",
"admin.dashboard.monthly_retention": "User retention rate by month after sign-up",
"admin.dashboard.retention.average": "Averaĝa",
"admin.dashboard.retention.cohort": "Registriĝo monato",
"admin.dashboard.retention.cohort_size": "Novaj uzantoj",
"alert.rate_limited.message": "Bonvolu reprovi post {retry_time, time, medium}.",
"alert.rate_limited.title": "Mesaĝkvante limigita",
"alert.unexpected.message": "Neatendita eraro okazis.",
"alert.unexpected.title": "Ups!",
"announcement.announcement": "Anonco",
"attachments_list.unprocessed": "(unprocessed)",
"attachments_list.unprocessed": "(neprilaborita)",
"autosuggest_hashtag.per_week": "{count} semajne",
"boost_modal.combo": "Vi povas premi {combo} por preterpasi sekvafoje",
"bundle_column_error.body": "Io misfunkciis en la ŝargado de ĉi tiu elemento.",
@ -104,6 +105,7 @@
"compose_form.poll.switch_to_single": "Ŝanĝi la balotenketon por permesi unu solan elekton",
"compose_form.publish": "Hup",
"compose_form.publish_loud": "{publish}!",
"compose_form.save_changes": "Save changes",
"compose_form.sensitive.hide": "Marki la aŭdovidaĵojn kiel tiklaj",
"compose_form.sensitive.marked": "Aŭdovidaĵo markita tikla",
"compose_form.sensitive.unmarked": "Aŭdovidaĵo ne markita tikla",
@ -118,7 +120,7 @@
"confirmations.delete.message": "Ĉu vi certas, ke vi volas forigi ĉi tiun mesaĝon?",
"confirmations.delete_list.confirm": "Forigi",
"confirmations.delete_list.message": "Ĉu vi certas, ke vi volas porĉiame forigi ĉi tiun liston?",
"confirmations.discard_edit_media.confirm": "Discard",
"confirmations.discard_edit_media.confirm": "Ne konservi",
"confirmations.discard_edit_media.message": "You have unsaved changes to the media description or preview, discard them anyway?",
"confirmations.domain_block.confirm": "Bloki la tutan domajnon",
"confirmations.domain_block.message": "Ĉu vi vere, vere certas, ke vi volas tute bloki {domain}? Plej ofte, trafa blokado kaj silentigado sufiĉas kaj preferindas. Vi ne vidos enhavon de tiu domajno en publika templinio aŭ en viaj sciigoj. Viaj sekvantoj de tiu domajno estos forigitaj.",
@ -312,7 +314,7 @@
"notifications.column_settings.favourite": "Stelumoj:",
"notifications.column_settings.filter_bar.advanced": "Montri ĉiujn kategoriojn",
"notifications.column_settings.filter_bar.category": "Rapida filtra breto",
"notifications.column_settings.filter_bar.show": "Montri",
"notifications.column_settings.filter_bar.show_bar": "Show filter bar",
"notifications.column_settings.follow": "Novaj sekvantoj:",
"notifications.column_settings.follow_request": "Novaj petoj de sekvado:",
"notifications.column_settings.mention": "Mencioj:",
@ -322,7 +324,8 @@
"notifications.column_settings.show": "Montri en kolumno",
"notifications.column_settings.sound": "Eligi sonon",
"notifications.column_settings.status": "Novaj mesaĝoj:",
"notifications.column_settings.unread_markers.category": "Unread notification markers",
"notifications.column_settings.unread_notifications.category": "Nelegitaj sciigoj",
"notifications.column_settings.unread_notifications.highlight": "Highlight unread notifications",
"notifications.filter.all": "Ĉiuj",
"notifications.filter.boosts": "Diskonigoj",
"notifications.filter.favourites": "Stelumoj",
@ -346,7 +349,7 @@
"poll.total_votes": "{count, plural, one {# voĉdono} other {# voĉdonoj}}",
"poll.vote": "Voĉdoni",
"poll.voted": "Vi elektis por ĉi tiu respondo",
"poll.votes": "{votes, plural, one {# vote} other {# votes}}",
"poll.votes": "{votes, plural, one {# voĉdono} other {# voĉdonoj}}",
"poll_button.add_poll": "Aldoni balotenketon",
"poll_button.remove_poll": "Forigi balotenketon",
"privacy.change": "Agordi mesaĝan privatecon",
@ -362,12 +365,20 @@
"regeneration_indicator.label": "Ŝargado…",
"regeneration_indicator.sublabel": "Via hejma fluo pretiĝas!",
"relative_time.days": "{number}t",
"relative_time.full.days": "{number, plural, one {# day} other {# days}} ago",
"relative_time.full.hours": "{number, plural, one {# hour} other {# hours}} ago",
"relative_time.full.just_now": "just now",
"relative_time.full.minutes": "{number, plural, one {# minute} other {# minutes}} ago",
"relative_time.full.seconds": "{number, plural, one {# second} other {# seconds}} ago",
"relative_time.hours": "{number}h",
"relative_time.just_now": "nun",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"relative_time.today": "hodiaŭ",
"reply_indicator.cancel": "Nuligi",
"report.categories.other": "Other",
"report.categories.spam": "Spam",
"report.categories.violation": "Content violates one or more server rules",
"report.forward": "Plusendi al {target}",
"report.forward_hint": "La konto estas en alia servilo. Ĉu sendi sennomigitan kopion de la signalo ankaŭ tien?",
"report.hint": "La signalo estos sendita al la kontrolantoj de via servilo. Vi povas doni klarigon pri kial vi signalas ĉi tiun konton sube:",
@ -396,9 +407,14 @@
"status.delete": "Forigi",
"status.detailed_status": "Detala konversacia vido",
"status.direct": "Rekte mesaĝi @{name}",
"status.edit": "Edit",
"status.edited": "Edited {date}",
"status.edited_x_times": "Edited {count, plural, one {{count} time} other {{count} times}}",
"status.embed": "Enkorpigi",
"status.favourite": "Stelumi",
"status.filtered": "Filtrita",
"status.history.created": "{name} created {date}",
"status.history.edited": "{name} edited {date}",
"status.load_more": "Ŝargi pli",
"status.media_hidden": "Aŭdovidaĵo kaŝita",
"status.mention": "Mencii @{name}",
@ -462,7 +478,7 @@
"upload_form.video_description": "Priskribi por homoj kiuj malfacile aŭdi aŭ vidi",
"upload_modal.analyzing_picture": "Bilda analizado…",
"upload_modal.apply": "Apliki",
"upload_modal.applying": "Applying…",
"upload_modal.applying": "Apliki…",
"upload_modal.choose_image": "Elekti bildon",
"upload_modal.description_placeholder": "Laŭ Ludoviko Zamenhof bongustas freŝa ĉeĥa manĝaĵo kun spicoj",
"upload_modal.detect_text": "Detekti tekston de la bildo",

View File

@ -47,16 +47,17 @@
"account.unmute": "Dejar de silenciar a @{name}",
"account.unmute_notifications": "Dejar de silenciar las notificaciones de @{name}",
"account_note.placeholder": "Hacé clic par agregar una nota",
"admin.dashboard.retention": "Retention",
"admin.dashboard.retention.average": "Average",
"admin.dashboard.retention.cohort": "Sign-up month",
"admin.dashboard.retention.cohort_size": "New users",
"admin.dashboard.daily_retention": "User retention rate by day after sign-up",
"admin.dashboard.monthly_retention": "User retention rate by month after sign-up",
"admin.dashboard.retention.average": "Promedio",
"admin.dashboard.retention.cohort": "Mes de registro",
"admin.dashboard.retention.cohort_size": "Nuevos usuarios",
"alert.rate_limited.message": "Por favor, reintentá después de las {retry_time, time, medium}.",
"alert.rate_limited.title": "Acción limitada",
"alert.unexpected.message": "Ocurrió un error.",
"alert.unexpected.title": "¡Epa!",
"announcement.announcement": "Anuncio",
"attachments_list.unprocessed": "(unprocessed)",
"attachments_list.unprocessed": "[sin procesar]",
"autosuggest_hashtag.per_week": "{count} por semana",
"boost_modal.combo": "Podés hacer clic en {combo} para saltar esto la próxima vez",
"bundle_column_error.body": "Algo salió mal al cargar este componente.",
@ -104,6 +105,7 @@
"compose_form.poll.switch_to_single": "Cambiar encuesta para permitir una sola opción",
"compose_form.publish": "Enviar",
"compose_form.publish_loud": "¡{publish}!",
"compose_form.save_changes": "Save changes",
"compose_form.sensitive.hide": "Marcar medio como sensible",
"compose_form.sensitive.marked": "{count, plural, one {El medio está marcado como sensible} other {Los medios están marcados como sensibles}}",
"compose_form.sensitive.unmarked": "El medio no está marcado como sensible",
@ -118,8 +120,8 @@
"confirmations.delete.message": "¿Estás seguro que querés eliminar este mensaje?",
"confirmations.delete_list.confirm": "Eliminar",
"confirmations.delete_list.message": "¿Estás seguro que querés eliminar permanentemente esta lista?",
"confirmations.discard_edit_media.confirm": "Discard",
"confirmations.discard_edit_media.message": "You have unsaved changes to the media description or preview, discard them anyway?",
"confirmations.discard_edit_media.confirm": "Descartar",
"confirmations.discard_edit_media.message": "Tenés cambios sin guardar en la descripción de medios o en la vista previa, ¿querés descartarlos de todos modos?",
"confirmations.domain_block.confirm": "Bloquear dominio entero",
"confirmations.domain_block.message": "¿Estás completamente seguro que querés bloquear el {domain} entero? En la mayoría de los casos, unos cuantos bloqueos y silenciados puntuales son suficientes y preferibles. No vas a ver contenido de ese dominio en ninguna de tus líneas temporales o en tus notificaciones. Tus seguidores de ese dominio serán quitados.",
"confirmations.logout.confirm": "Cerrar sesión",
@ -267,7 +269,7 @@
"lists.replies_policy.title": "Mostrar respuestas a:",
"lists.search": "Buscar entre la gente que seguís",
"lists.subheading": "Tus listas",
"load_pending": "{count, plural, one {# nuevo elemento} other {# nuevos elementos}}",
"load_pending": "{count, plural, one {# elemento nuevo} other {# elementos nuevos}}",
"loading_indicator.label": "Cargando...",
"media_gallery.toggle_visible": "Ocultar {number, plural, one {imagen} other {imágenes}}",
"missing_indicator.label": "No se encontró",
@ -312,7 +314,7 @@
"notifications.column_settings.favourite": "Favoritos:",
"notifications.column_settings.filter_bar.advanced": "Mostrar todas las categorías",
"notifications.column_settings.filter_bar.category": "Barra de filtrado rápido",
"notifications.column_settings.filter_bar.show": "Mostrar",
"notifications.column_settings.filter_bar.show_bar": "Mostrar barra de filtros",
"notifications.column_settings.follow": "Nuevos seguidores:",
"notifications.column_settings.follow_request": "Nuevas solicitudes de seguimiento:",
"notifications.column_settings.mention": "Menciones:",
@ -322,7 +324,8 @@
"notifications.column_settings.show": "Mostrar en columna",
"notifications.column_settings.sound": "Reproducir sonido",
"notifications.column_settings.status": "Nuevos mensajes:",
"notifications.column_settings.unread_markers.category": "Indicadores de notificaciones no leídas",
"notifications.column_settings.unread_notifications.category": "Notificaciones sin leer",
"notifications.column_settings.unread_notifications.highlight": "Resaltar notificaciones no leídas",
"notifications.filter.all": "Todas",
"notifications.filter.boosts": "Adhesiones",
"notifications.filter.favourites": "Favoritos",
@ -346,7 +349,7 @@
"poll.total_votes": "{count, plural, one {# voto} other {# votos}}",
"poll.vote": "Votar",
"poll.voted": "Votaste esta opción",
"poll.votes": "{votes, plural, one {# vote} other {# votes}}",
"poll.votes": "{votes, plural, one {# voto} other {# votos}}",
"poll_button.add_poll": "Agregar encuesta",
"poll_button.remove_poll": "Quitar encuesta",
"privacy.change": "Configurar privacidad del mensaje",
@ -362,12 +365,20 @@
"regeneration_indicator.label": "Cargando…",
"regeneration_indicator.sublabel": "¡Se está preparando tu línea temporal principal!",
"relative_time.days": "{number}d",
"relative_time.full.days": "{number, plural, one {# day} other {# days}} ago",
"relative_time.full.hours": "{number, plural, one {# hour} other {# hours}} ago",
"relative_time.full.just_now": "just now",
"relative_time.full.minutes": "{number, plural, one {# minute} other {# minutes}} ago",
"relative_time.full.seconds": "{number, plural, one {# second} other {# seconds}} ago",
"relative_time.hours": "{number}h",
"relative_time.just_now": "ahora",
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"relative_time.today": "hoy",
"reply_indicator.cancel": "Cancelar",
"report.categories.other": "Other",
"report.categories.spam": "Spam",
"report.categories.violation": "Content violates one or more server rules",
"report.forward": "Reenviar a {target}",
"report.forward_hint": "La cuenta es de otro servidor. ¿Querés enviar una copia anonimizada del informe también ahí?",
"report.hint": "La denuncia se enviará a los moderadores de tu servidor. A continuación, podés proporcionar una explicación de por qué estás denunciando esta cuenta:",
@ -396,9 +407,14 @@
"status.delete": "Eliminar",
"status.detailed_status": "Vista de conversación detallada",
"status.direct": "Mensaje directo para @{name}",
"status.edit": "Edit",
"status.edited": "Edited {date}",
"status.edited_x_times": "Edited {count, plural, one {{count} time} other {{count} times}}",
"status.embed": "Insertar",
"status.favourite": "Marcar como favorito",
"status.filtered": "Filtrado",
"status.history.created": "{name} created {date}",
"status.history.edited": "{name} edited {date}",
"status.load_more": "Cargar más",
"status.media_hidden": "Medios ocultos",
"status.mention": "Mencionar a @{name}",
@ -462,7 +478,7 @@
"upload_form.video_description": "Agregá una descripción para personas con dificultades auditivas o visuales",
"upload_modal.analyzing_picture": "Analizando imagen…",
"upload_modal.apply": "Aplicar",
"upload_modal.applying": "Applying…",
"upload_modal.applying": "Aplicando…",
"upload_modal.choose_image": "Elegir imagen",
"upload_modal.description_placeholder": "El veloz murciélago hindú comía feliz cardillo y kiwi. La cigüeña tocaba el saxofón detrás del palenque de paja.",
"upload_modal.detect_text": "Detectar texto de la imagen",

View File

@ -1,6 +1,6 @@
{
"account.account_note_header": "Nota",
"account.add_or_remove_from_list": "Agregar o eliminar de listas",
"account.add_or_remove_from_list": "Agregar o eliminar de las listas",
"account.badges.bot": "Bot",
"account.badges.group": "Grupo",
"account.block": "Bloquear a @{name}",
@ -47,16 +47,17 @@
"account.unmute": "Dejar de silenciar a @{name}",
"account.unmute_notifications": "Dejar de silenciar las notificaciones de @{name}",
"account_note.placeholder": "Clic para añadir nota",
"admin.dashboard.retention": "Retention",
"admin.dashboard.retention.average": "Average",
"admin.dashboard.retention.cohort": "Sign-up month",
"admin.dashboard.retention.cohort_size": "New users",
"admin.dashboard.daily_retention": "User retention rate by day after sign-up",
"admin.dashboard.monthly_retention": "User retention rate by month after sign-up",
"admin.dashboard.retention.average": "Promedio",
"admin.dashboard.retention.cohort": "Mes de registro",
"admin.dashboard.retention.cohort_size": "Nuevos usuarios",
"alert.rate_limited.message": "Por favor reintente después de {retry_time, time, medium}.",
"alert.rate_limited.title": "Tarifa limitada",
"alert.unexpected.message": "Hubo un error inesperado.",
"alert.unexpected.title": "¡Ups!",
"announcement.announcement": "Anuncio",
"attachments_list.unprocessed": "(unprocessed)",
"attachments_list.unprocessed": "(sin procesar)",
"autosuggest_hashtag.per_week": "{count} por semana",
"boost_modal.combo": "Puedes hacer clic en {combo} para saltar este aviso la próxima vez",
"bundle_column_error.body": "Algo salió mal al cargar este componente.",
@ -104,6 +105,7 @@
"compose_form.poll.switch_to_single": "Modificar encuesta para permitir una única opción",
"compose_form.publish": "Tootear",
"compose_form.publish_loud": "¡{publish}!",
"compose_form.save_changes": "Save changes",
"compose_form.sensitive.hide": "Marcar multimedia como sensible",
"compose_form.sensitive.marked": "Material marcado como sensible",
"compose_form.sensitive.unmarked": "Material no marcado como sensible",
@ -118,8 +120,8 @@
"confirmations.delete.message": "¿Estás seguro de que quieres borrar este toot?",
"confirmations.delete_list.confirm": "Eliminar",
"confirmations.delete_list.message": "¿Seguro que quieres borrar esta lista permanentemente?",
"confirmations.discard_edit_media.confirm": "Discard",
"confirmations.discard_edit_media.message": "You have unsaved changes to the media description or preview, discard them anyway?",
"confirmations.discard_edit_media.confirm": "Descartar",
"confirmations.discard_edit_media.message": "Tienes cambios sin guardar en la descripción o vista previa del archivo, ¿deseas descartarlos de cualquier manera?",
"confirmations.domain_block.confirm": "Ocultar dominio entero",
"confirmations.domain_block.message": "¿Seguro de que quieres bloquear al dominio {domain} entero? En general unos cuantos bloqueos y silenciados concretos es suficiente y preferible.",
"confirmations.logout.confirm": "Cerrar sesión",
@ -312,7 +314,7 @@
"notifications.column_settings.favourite": "Favoritos:",
"notifications.column_settings.filter_bar.advanced": "Mostrar todas las categorías",
"notifications.column_settings.filter_bar.category": "Barra de filtrado rápido",
"notifications.column_settings.filter_bar.show": "Mostrar",
"notifications.column_settings.filter_bar.show_bar": "Mostrar barra de filtros",
"notifications.column_settings.follow": "Nuevos seguidores:",
"notifications.column_settings.follow_request": "Nuevas solicitudes de seguimiento:",
"notifications.column_settings.mention": "Menciones:",
@ -322,7 +324,8 @@
"notifications.column_settings.show": "Mostrar en columna",
"notifications.column_settings.sound": "Reproducir sonido",
"notifications.column_settings.status": "Nuevos toots:",
"notifications.column_settings.unread_markers.category": "Indicadores de notificaciones no leídas",
"notifications.column_settings.unread_notifications.category": "Notificaciones sin leer",
"notifications.column_settings.unread_notifications.highlight": "Destacar notificaciones no leídas",
"notifications.filter.all": "Todos",
"notifications.filter.boosts": "Retoots",
"notifications.filter.favourites": "Favoritos",
@ -346,7 +349,7 @@
"poll.total_votes": "{count, plural, one {# voto} other {# votos}}",
"poll.vote": "Votar",
"poll.voted": "Has votado a favor de esta respuesta",
"poll.votes": "{votes, plural, one {# vote} other {# votes}}",
"poll.votes": "{votes, plural, one {# voto} other {# votos}}",
"poll_button.add_poll": "Añadir una encuesta",
"poll_button.remove_poll": "Eliminar encuesta",
"privacy.change": "Ajustar privacidad",
@ -362,12 +365,20 @@
"regeneration_indicator.label": "Cargando…",
"regeneration_indicator.sublabel": "¡Tu historia de inicio se está preparando!",
"relative_time.days": "{number} d",
"relative_time.full.days": "{number, plural, one {# day} other {# days}} ago",
"relative_time.full.hours": "{number, plural, one {# hour} other {# hours}} ago",
"relative_time.full.just_now": "just now",
"relative_time.full.minutes": "{number, plural, one {# minute} other {# minutes}} ago",
"relative_time.full.seconds": "{number, plural, one {# second} other {# seconds}} ago",
"relative_time.hours": "{number} h",
"relative_time.just_now": "ahora",
"relative_time.minutes": "{number} m",
"relative_time.seconds": "{number} s",
"relative_time.today": "hoy",
"reply_indicator.cancel": "Cancelar",
"report.categories.other": "Other",
"report.categories.spam": "Spam",
"report.categories.violation": "Content violates one or more server rules",
"report.forward": "Reenviar a {target}",
"report.forward_hint": "Esta cuenta es de otro servidor. ¿Enviar una copia anonimizada del informe allí también?",
"report.hint": "El informe se enviará a los moderadores de tu instancia. Puedes proporcionar una explicación de por qué informas sobre esta cuenta a continuación:",
@ -396,9 +407,14 @@
"status.delete": "Borrar",
"status.detailed_status": "Vista de conversación detallada",
"status.direct": "Mensaje directo a @{name}",
"status.edit": "Edit",
"status.edited": "Edited {date}",
"status.edited_x_times": "Edited {count, plural, one {{count} time} other {{count} times}}",
"status.embed": "Incrustado",
"status.favourite": "Favorito",
"status.filtered": "Filtrado",
"status.history.created": "{name} created {date}",
"status.history.edited": "{name} edited {date}",
"status.load_more": "Cargar más",
"status.media_hidden": "Contenido multimedia oculto",
"status.mention": "Mencionar",
@ -462,7 +478,7 @@
"upload_form.video_description": "Describir para personas con problemas auditivos o visuales",
"upload_modal.analyzing_picture": "Analizando imagen…",
"upload_modal.apply": "Aplicar",
"upload_modal.applying": "Applying…",
"upload_modal.applying": "Aplicando…",
"upload_modal.choose_image": "Elegir imagen",
"upload_modal.description_placeholder": "Un rápido zorro marrón salta sobre el perro perezoso",
"upload_modal.detect_text": "Detectar texto de la imagen",

View File

@ -47,16 +47,17 @@
"account.unmute": "Dejar de silenciar a @{name}",
"account.unmute_notifications": "Dejar de silenciar las notificaciones de @{name}",
"account_note.placeholder": "Clic para añadir nota",
"admin.dashboard.retention": "Retention",
"admin.dashboard.retention.average": "Average",
"admin.dashboard.retention.cohort": "Sign-up month",
"admin.dashboard.retention.cohort_size": "New users",
"admin.dashboard.daily_retention": "User retention rate by day after sign-up",
"admin.dashboard.monthly_retention": "User retention rate by month after sign-up",
"admin.dashboard.retention.average": "Media",
"admin.dashboard.retention.cohort": "Mes de registro",
"admin.dashboard.retention.cohort_size": "Nuevos usuarios",
"alert.rate_limited.message": "Por favor reintente después de {retry_time, time, medium}.",
"alert.rate_limited.title": "Tarifa limitada",
"alert.unexpected.message": "Hubo un error inesperado.",
"alert.unexpected.title": "¡Ups!",
"announcement.announcement": "Anuncio",
"attachments_list.unprocessed": "(unprocessed)",
"attachments_list.unprocessed": "(sin procesar)",
"autosuggest_hashtag.per_week": "{count} por semana",
"boost_modal.combo": "Puedes hacer clic en {combo} para saltar este aviso la próxima vez",
"bundle_column_error.body": "Algo salió mal al cargar este componente.",
@ -104,6 +105,7 @@
"compose_form.poll.switch_to_single": "Modificar encuesta para permitir una única opción",
"compose_form.publish": "Tootear",
"compose_form.publish_loud": "{publish}!",
"compose_form.save_changes": "Save changes",
"compose_form.sensitive.hide": "{count, plural, one {Marcar material como sensible} other {Marcar material como sensible}}",
"compose_form.sensitive.marked": "{count, plural, one {Material marcado como sensible} other {Material marcado como sensible}}",
"compose_form.sensitive.unmarked": "{count, plural, one {Material no marcado como sensible} other {Material no marcado como sensible}}",
@ -118,8 +120,8 @@
"confirmations.delete.message": "¿Estás seguro de que quieres borrar esta publicación?",
"confirmations.delete_list.confirm": "Eliminar",
"confirmations.delete_list.message": "¿Seguro que quieres borrar esta lista permanentemente?",
"confirmations.discard_edit_media.confirm": "Discard",
"confirmations.discard_edit_media.message": "You have unsaved changes to the media description or preview, discard them anyway?",
"confirmations.discard_edit_media.confirm": "Descartar",
"confirmations.discard_edit_media.message": "Tienes cambios sin guardar en la descripción o vista previa del archivo audiovisual, ¿descartarlos de todos modos?",
"confirmations.domain_block.confirm": "Ocultar dominio entero",
"confirmations.domain_block.message": "¿Seguro de que quieres bloquear al dominio {domain} entero? En general unos cuantos bloqueos y silenciados concretos es suficiente y preferible.",
"confirmations.logout.confirm": "Cerrar sesión",
@ -312,7 +314,7 @@
"notifications.column_settings.favourite": "Favoritos:",
"notifications.column_settings.filter_bar.advanced": "Mostrar todas las categorías",
"notifications.column_settings.filter_bar.category": "Barra de filtrado rápido",
"notifications.column_settings.filter_bar.show": "Mostrar",
"notifications.column_settings.filter_bar.show_bar": "Mostrar barra de filtros",
"notifications.column_settings.follow": "Nuevos seguidores:",
"notifications.column_settings.follow_request": "Nuevas solicitudes de seguimiento:",
"notifications.column_settings.mention": "Menciones:",
@ -322,7 +324,8 @@
"notifications.column_settings.show": "Mostrar en columna",
"notifications.column_settings.sound": "Reproducir sonido",
"notifications.column_settings.status": "Nuevas publicaciones:",
"notifications.column_settings.unread_markers.category": "Indicadores de notificaciones no leídas",
"notifications.column_settings.unread_notifications.category": "Notificaciones sin leer",
"notifications.column_settings.unread_notifications.highlight": "Destacar notificaciones no leídas",
"notifications.filter.all": "Todos",
"notifications.filter.boosts": "Retoots",
"notifications.filter.favourites": "Favoritos",
@ -346,7 +349,7 @@
"poll.total_votes": "{count, plural, one {# voto} other {# votos}}",
"poll.vote": "Votar",
"poll.voted": "Has votado a favor de esta respuesta",
"poll.votes": "{votes, plural, one {# vote} other {# votes}}",
"poll.votes": "{votes, plural, one {# voto} other {# votos}}",
"poll_button.add_poll": "Añadir una encuesta",
"poll_button.remove_poll": "Eliminar encuesta",
"privacy.change": "Ajustar privacidad",
@ -362,12 +365,20 @@
"regeneration_indicator.label": "Cargando…",
"regeneration_indicator.sublabel": "¡Tu historia de inicio se está preparando!",
"relative_time.days": "{number} d",
"relative_time.full.days": "{number, plural, one {# day} other {# days}} ago",
"relative_time.full.hours": "{number, plural, one {# hour} other {# hours}} ago",
"relative_time.full.just_now": "just now",
"relative_time.full.minutes": "{number, plural, one {# minute} other {# minutes}} ago",
"relative_time.full.seconds": "{number, plural, one {# second} other {# seconds}} ago",
"relative_time.hours": "{number} h",
"relative_time.just_now": "ahora",
"relative_time.minutes": "{number} m",
"relative_time.seconds": "{number} s",
"relative_time.today": "hoy",
"reply_indicator.cancel": "Cancelar",
"report.categories.other": "Other",
"report.categories.spam": "Spam",
"report.categories.violation": "Content violates one or more server rules",
"report.forward": "Reenviar a {target}",
"report.forward_hint": "Esta cuenta es de otro servidor. ¿Enviar una copia anonimizada del informe allí también?",
"report.hint": "El informe se enviará a los moderadores de tu instancia. Puedes proporcionar una explicación de por qué informas sobre esta cuenta a continuación:",
@ -396,9 +407,14 @@
"status.delete": "Borrar",
"status.detailed_status": "Vista de conversación detallada",
"status.direct": "Mensaje directo a @{name}",
"status.edit": "Edit",
"status.edited": "Edited {date}",
"status.edited_x_times": "Edited {count, plural, one {{count} time} other {{count} times}}",
"status.embed": "Incrustado",
"status.favourite": "Favorito",
"status.filtered": "Filtrado",
"status.history.created": "{name} created {date}",
"status.history.edited": "{name} edited {date}",
"status.load_more": "Cargar más",
"status.media_hidden": "Contenido multimedia oculto",
"status.mention": "Mencionar",
@ -462,7 +478,7 @@
"upload_form.video_description": "Describir para personas con problemas auditivos o visuales",
"upload_modal.analyzing_picture": "Analizando imagen…",
"upload_modal.apply": "Aplicar",
"upload_modal.applying": "Applying…",
"upload_modal.applying": "Aplicando…",
"upload_modal.choose_image": "Elegir imagen",
"upload_modal.description_placeholder": "Un rápido zorro marrón salta sobre el perro perezoso",
"upload_modal.detect_text": "Detectar texto de la imagen",

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