Merge commit '57c5493d4e7a42ad9e8fec20d71c20bee8674287' into merging-upstream

th-downstream
Ondřej Hruška 7 years ago
commit 914c0465b1

@ -101,11 +101,19 @@ SMTP_FROM_ADDRESS=notifications@example.com
# Swift (optional) # Swift (optional)
# SWIFT_ENABLED=true # SWIFT_ENABLED=true
# SWIFT_USERNAME= # SWIFT_USERNAME=
# For Keystone V3, the value for SWIFT_TENANT should be the project name
# SWIFT_TENANT= # SWIFT_TENANT=
# SWIFT_PASSWORD= # SWIFT_PASSWORD=
# Keystone V2 and V3 URLs are supported. Use a V3 URL if possible to avoid
# issues with token rate-limiting during high load.
# SWIFT_AUTH_URL= # SWIFT_AUTH_URL=
# SWIFT_CONTAINER= # SWIFT_CONTAINER=
# SWIFT_OBJECT_URL= # SWIFT_OBJECT_URL=
# SWIFT_REGION=
# Defaults to 'default'
# SWIFT_DOMAIN_NAME=
# Defaults to 60 seconds. Set to 0 to disable
# SWIFT_CACHE_TTL=
# Optional alias for S3 if you want to use Cloudfront or Cloudflare in front # Optional alias for S3 if you want to use Cloudfront or Cloudflare in front
# S3_CLOUDFRONT_HOST= # S3_CLOUDFRONT_HOST=

@ -1 +1 @@
2.4.1 2.4.2

@ -26,18 +26,16 @@ addons:
postgresql: 9.4 postgresql: 9.4
apt: apt:
sources: sources:
- ubuntu-toolchain-r-test
- trusty-media - trusty-media
packages: packages:
- ffmpeg - ffmpeg
- g++-6
- libprotobuf-dev - libprotobuf-dev
- protobuf-compiler - protobuf-compiler
- libicu-dev - libicu-dev
rvm: rvm:
- 2.3.4 - 2.3.4
- 2.4.1 - 2.4.2
services: services:
- redis-server - redis-server

@ -1,4 +1,5 @@
ffmpeg ffmpeg
libicu[0-9][0-9]
libicu-dev libicu-dev
libidn11 libidn11
libidn11-dev libidn11-dev

@ -1,4 +1,4 @@
FROM ruby:2.4.1-alpine3.6 FROM ruby:2.4.2-alpine3.6
LABEL maintainer="https://github.com/tootsuite/mastodon" \ LABEL maintainer="https://github.com/tootsuite/mastodon" \
description="A GNU Social-compatible microblogging server" description="A GNU Social-compatible microblogging server"

@ -5,8 +5,8 @@ ruby '>= 2.3.0', '< 2.5.0'
gem 'pkg-config', '~> 1.2' gem 'pkg-config', '~> 1.2'
gem 'puma', '~> 3.8' gem 'puma', '~> 3.10'
gem 'rails', '~> 5.1.0' gem 'rails', '~> 5.1.4'
gem 'uglifier', '~> 3.2' gem 'uglifier', '~> 3.2'
gem 'hamlit-rails', '~> 0.2' gem 'hamlit-rails', '~> 0.2'
@ -25,7 +25,7 @@ gem 'bootsnap'
gem 'browser' gem 'browser'
gem 'charlock_holmes', '~> 0.7.5' gem 'charlock_holmes', '~> 0.7.5'
gem 'iso-639' gem 'iso-639'
gem 'cld3', '~> 3.1' gem 'cld3', '~> 3.2.0'
gem 'devise', '~> 4.2' gem 'devise', '~> 4.2'
gem 'devise-two-factor', '~> 3.0' gem 'devise-two-factor', '~> 3.0'
gem 'doorkeeper', '~> 4.2' gem 'doorkeeper', '~> 4.2'

@ -1,25 +1,25 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (5.1.3) actioncable (5.1.4)
actionpack (= 5.1.3) actionpack (= 5.1.4)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (~> 0.6.1) websocket-driver (~> 0.6.1)
actionmailer (5.1.3) actionmailer (5.1.4)
actionpack (= 5.1.3) actionpack (= 5.1.4)
actionview (= 5.1.3) actionview (= 5.1.4)
activejob (= 5.1.3) activejob (= 5.1.4)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
actionpack (5.1.3) actionpack (5.1.4)
actionview (= 5.1.3) actionview (= 5.1.4)
activesupport (= 5.1.3) activesupport (= 5.1.4)
rack (~> 2.0) rack (~> 2.0)
rack-test (~> 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2) rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.1.3) actionview (5.1.4)
activesupport (= 5.1.3) activesupport (= 5.1.4)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
@ -30,16 +30,16 @@ GEM
case_transform (>= 0.2) case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.2)
active_record_query_trace (1.5.4) active_record_query_trace (1.5.4)
activejob (5.1.3) activejob (5.1.4)
activesupport (= 5.1.3) activesupport (= 5.1.4)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (5.1.3) activemodel (5.1.4)
activesupport (= 5.1.3) activesupport (= 5.1.4)
activerecord (5.1.3) activerecord (5.1.4)
activemodel (= 5.1.3) activemodel (= 5.1.4)
activesupport (= 5.1.3) activesupport (= 5.1.4)
arel (~> 8.0) arel (~> 8.0)
activesupport (5.1.3) activesupport (5.1.4)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (~> 0.7) i18n (~> 0.7)
minitest (~> 5.1) minitest (~> 5.1)
@ -57,33 +57,33 @@ GEM
encryptor (~> 3.0.0) encryptor (~> 3.0.0)
av (0.9.0) av (0.9.0)
cocaine (~> 0.5.3) cocaine (~> 0.5.3)
aws-sdk (2.10.21) aws-sdk (2.10.46)
aws-sdk-resources (= 2.10.21) aws-sdk-resources (= 2.10.46)
aws-sdk-core (2.10.21) aws-sdk-core (2.10.46)
aws-sigv4 (~> 1.0) aws-sigv4 (~> 1.0)
jmespath (~> 1.0) jmespath (~> 1.0)
aws-sdk-resources (2.10.21) aws-sdk-resources (2.10.46)
aws-sdk-core (= 2.10.21) aws-sdk-core (= 2.10.46)
aws-sigv4 (1.0.1) aws-sigv4 (1.0.2)
bcrypt (3.1.11) bcrypt (3.1.11)
better_errors (2.1.1) better_errors (2.3.0)
coderay (>= 1.0.0) coderay (>= 1.0.0)
erubis (>= 2.6.6) erubi (>= 1.0.0)
rack (>= 0.9.0) rack (>= 0.9.0)
binding_of_caller (0.7.2) binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
bootsnap (1.1.2) bootsnap (1.1.3)
msgpack (~> 1.0) msgpack (~> 1.0)
brakeman (3.7.2) brakeman (3.7.2)
browser (2.4.0) browser (2.5.1)
builder (3.2.3) builder (3.2.3)
bullet (5.5.1) bullet (5.6.1)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.10.0) uniform_notifier (~> 1.10.0)
bundler-audit (0.6.0) bundler-audit (0.6.0)
bundler (~> 1.2) bundler (~> 1.2)
thor (~> 0.18) thor (~> 0.18)
capistrano (3.8.2) capistrano (3.9.1)
airbrussh (>= 1.0.0) airbrussh (>= 1.0.0)
i18n i18n
rake (>= 10.0.0) rake (>= 10.0.0)
@ -99,9 +99,9 @@ GEM
sshkit (~> 1.3) sshkit (~> 1.3)
capistrano-yarn (2.0.2) capistrano-yarn (2.0.2)
capistrano (~> 3.0) capistrano (~> 3.0)
capybara (2.14.4) capybara (2.15.1)
addressable addressable
mime-types (>= 1.16) mini_mime (>= 0.1.3)
nokogiri (>= 1.3.3) nokogiri (>= 1.3.3)
rack (>= 1.0.0) rack (>= 1.0.0)
rack-test (>= 0.5.4) rack-test (>= 0.5.4)
@ -110,12 +110,12 @@ GEM
activesupport activesupport
charlock_holmes (0.7.5) charlock_holmes (0.7.5)
chunky_png (1.3.8) chunky_png (1.3.8)
cld3 (3.1.3) cld3 (3.2.0)
ffi (>= 1.1.0, < 1.10.0) ffi (>= 1.1.0, < 1.10.0)
climate_control (0.2.0) climate_control (0.2.0)
cocaine (0.5.8) cocaine (0.5.8)
climate_control (>= 0.0.3, < 1.0) climate_control (>= 0.0.3, < 1.0)
coderay (1.1.1) coderay (1.1.2)
colorize (0.8.1) colorize (0.8.1)
concurrent-ruby (1.0.5) concurrent-ruby (1.0.5)
connection_pool (2.2.1) connection_pool (2.2.1)
@ -151,13 +151,12 @@ GEM
thread_safe thread_safe
encryptor (3.0.0) encryptor (3.0.0)
erubi (1.6.1) erubi (1.6.1)
erubis (2.7.0)
et-orbi (1.0.5) et-orbi (1.0.5)
tzinfo tzinfo
excon (0.58.0) excon (0.59.0)
execjs (2.7.0) execjs (2.7.0)
fabrication (2.16.2) fabrication (2.16.3)
faker (1.7.3) faker (1.8.4)
i18n (~> 0.5) i18n (~> 0.5)
fast_blank (1.0.0) fast_blank (1.0.0)
ffi (1.9.18) ffi (1.9.18)
@ -194,7 +193,7 @@ GEM
railties (>= 4.0.1) railties (>= 4.0.1)
hamster (3.0.0) hamster (3.0.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
hashdiff (0.3.5) hashdiff (0.3.6)
highline (1.7.8) highline (1.7.8)
hiredis (0.6.1) hiredis (0.6.1)
hkdf (0.3.0) hkdf (0.3.0)
@ -213,11 +212,11 @@ GEM
colorize colorize
rack rack
i18n (0.8.6) i18n (0.8.6)
i18n-tasks (0.9.16) i18n-tasks (0.9.18)
activesupport (>= 4.0.2) activesupport (>= 4.0.2)
ast (>= 2.1.0) ast (>= 2.1.0)
easy_translate (>= 0.5.0) easy_translate (>= 0.5.0)
erubis erubi
highline (>= 1.7.3) highline (>= 1.7.3)
i18n i18n
parser (>= 2.2.3.0) parser (>= 2.2.3.0)
@ -231,7 +230,7 @@ GEM
json-ld (2.1.5) json-ld (2.1.5)
multi_json (~> 1.12) multi_json (~> 1.12)
rdf (~> 2.2) rdf (~> 2.2)
json-ld-preloaded (2.2.1) json-ld-preloaded (2.2.2)
json-ld (~> 2.1, >= 2.1.5) json-ld (~> 2.1, >= 2.1.5)
multi_json (~> 1.11) multi_json (~> 1.11)
rdf (~> 2.2) rdf (~> 2.2)
@ -258,10 +257,11 @@ GEM
letter_opener (~> 1.0) letter_opener (~> 1.0)
railties (>= 3.2) railties (>= 3.2)
link_header (0.0.8) link_header (0.0.8)
lograge (0.5.1) lograge (0.6.0)
actionpack (>= 4, < 5.2) actionpack (>= 4, < 5.2)
activesupport (>= 4, < 5.2) activesupport (>= 4, < 5.2)
railties (>= 4, < 5.2) railties (>= 4, < 5.2)
request_store (~> 1.0)
loofah (2.0.3) loofah (2.0.3)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
mail (2.6.6) mail (2.6.6)
@ -276,27 +276,28 @@ GEM
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521) mime-types-data (3.2016.0521)
mimemagic (0.3.2) mimemagic (0.3.2)
mini_mime (0.1.4)
mini_portile2 (2.2.0) mini_portile2 (2.2.0)
minitest (5.10.3) minitest (5.10.3)
msgpack (1.1.0) msgpack (1.1.0)
multi_json (1.12.1) multi_json (1.12.2)
net-scp (1.2.1) net-scp (1.2.1)
net-ssh (>= 2.6.5) net-ssh (>= 2.6.5)
net-ssh (4.1.0) net-ssh (4.2.0)
nio4r (2.1.0) nio4r (2.1.0)
nokogiri (1.8.0) nokogiri (1.8.0)
mini_portile2 (~> 2.2.0) mini_portile2 (~> 2.2.0)
nokogumbo (1.4.13) nokogumbo (1.4.13)
nokogiri nokogiri
oj (3.3.4) oj (3.3.5)
openssl (2.0.4) openssl (2.0.5)
orm_adapter (0.5.0) orm_adapter (0.5.0)
ostatus2 (2.0.1) ostatus2 (2.0.1)
addressable (~> 2.4) addressable (~> 2.4)
http (~> 2.0) http (~> 2.0)
nokogiri (~> 1.6) nokogiri (~> 1.6)
openssl (~> 2.0) openssl (~> 2.0)
ox (2.5.0) ox (2.6.0)
paperclip (5.1.0) paperclip (5.1.0)
activemodel (>= 4.2.0) activemodel (>= 4.2.0)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
@ -306,15 +307,15 @@ GEM
paperclip-av-transcoder (0.6.4) paperclip-av-transcoder (0.6.4)
av (~> 0.9.0) av (~> 0.9.0)
paperclip (>= 2.5.2) paperclip (>= 2.5.2)
parallel (1.11.2) parallel (1.12.0)
parallel_tests (2.14.2) parallel_tests (2.15.0)
parallel parallel
parser (2.4.0.0) parser (2.4.0.0)
ast (~> 2.2) ast (~> 2.2)
pg (0.21.0) pg (0.21.0)
pghero (1.7.0) pghero (1.7.0)
activerecord activerecord
pkg-config (1.2.4) pkg-config (1.2.7)
powerpack (0.1.1) powerpack (0.1.1)
pry (0.10.4) pry (0.10.4)
coderay (~> 1.1.0) coderay (~> 1.1.0)
@ -323,7 +324,7 @@ GEM
pry-rails (0.3.6) pry-rails (0.3.6)
pry (>= 0.10.4) pry (>= 0.10.4)
public_suffix (3.0.0) public_suffix (3.0.0)
puma (3.9.1) puma (3.10.0)
pundit (1.1.0) pundit (1.1.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
rabl (0.13.1) rabl (0.13.1)
@ -334,20 +335,20 @@ GEM
rack-cors (0.4.1) rack-cors (0.4.1)
rack-protection (2.0.0) rack-protection (2.0.0)
rack rack
rack-test (0.6.3) rack-test (0.7.0)
rack (>= 1.0) rack (>= 1.0, < 3)
rack-timeout (0.4.2) rack-timeout (0.4.2)
rails (5.1.3) rails (5.1.4)
actioncable (= 5.1.3) actioncable (= 5.1.4)
actionmailer (= 5.1.3) actionmailer (= 5.1.4)
actionpack (= 5.1.3) actionpack (= 5.1.4)
actionview (= 5.1.3) actionview (= 5.1.4)
activejob (= 5.1.3) activejob (= 5.1.4)
activemodel (= 5.1.3) activemodel (= 5.1.4)
activerecord (= 5.1.3) activerecord (= 5.1.4)
activesupport (= 5.1.3) activesupport (= 5.1.4)
bundler (>= 1.3.0) bundler (>= 1.3.0)
railties (= 5.1.3) railties (= 5.1.4)
sprockets-rails (>= 2.0.0) sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.2) rails-controller-testing (1.0.2)
actionpack (~> 5.x, >= 5.0.1) actionpack (~> 5.x, >= 5.0.1)
@ -363,16 +364,16 @@ GEM
railties (~> 5.0) railties (~> 5.0)
rails-settings-cached (0.6.6) rails-settings-cached (0.6.6)
rails (>= 4.2.0) rails (>= 4.2.0)
railties (5.1.3) railties (5.1.4)
actionpack (= 5.1.3) actionpack (= 5.1.4)
activesupport (= 5.1.3) activesupport (= 5.1.4)
method_source method_source
rake (>= 0.8.7) rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0) thor (>= 0.18.1, < 2.0)
rainbow (2.2.2) rainbow (2.2.2)
rake rake
rake (12.0.0) rake (12.1.0)
rdf (2.2.8) rdf (2.2.9)
hamster (~> 3.0) hamster (~> 3.0)
link_header (~> 0.0, >= 0.0.8) link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.3.2) rdf-normalize (0.3.2)
@ -396,6 +397,7 @@ GEM
redis-store (>= 1.2, < 2) redis-store (>= 1.2, < 2)
redis-store (1.3.0) redis-store (1.3.0)
redis (>= 2.2) redis (>= 2.2)
request_store (1.3.2)
responders (2.4.0) responders (2.4.0)
actionpack (>= 4.2.0, < 5.3) actionpack (>= 4.2.0, < 5.3)
railties (>= 4.2.0, < 5.3) railties (>= 4.2.0, < 5.3)
@ -410,7 +412,7 @@ GEM
rspec-mocks (3.6.0) rspec-mocks (3.6.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.6.0) rspec-support (~> 3.6.0)
rspec-rails (3.6.0) rspec-rails (3.6.1)
actionpack (>= 3.0) actionpack (>= 3.0)
activesupport (>= 3.0) activesupport (>= 3.0)
railties (>= 3.0) railties (>= 3.0)
@ -422,15 +424,15 @@ GEM
rspec-core (~> 3.0, >= 3.0.0) rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0) sidekiq (>= 2.4.0)
rspec-support (3.6.0) rspec-support (3.6.0)
rubocop (0.49.1) rubocop (0.50.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 2.3.3.1, < 3.0) parser (>= 2.3.3.1, < 3.0)
powerpack (~> 0.1) powerpack (~> 0.1)
rainbow (>= 1.99.1, < 3.0) rainbow (>= 2.2.2, < 3.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1) unicode-display_width (~> 1.0, >= 1.0.1)
ruby-oembed (0.12.0) ruby-oembed (0.12.0)
ruby-progressbar (1.8.1) ruby-progressbar (1.8.3)
rufus-scheduler (3.4.2) rufus-scheduler (3.4.2)
et-orbi (~> 1.0) et-orbi (~> 1.0)
safe_yaml (1.0.4) safe_yaml (1.0.4)
@ -438,7 +440,7 @@ GEM
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.4.4) nokogiri (>= 1.4.4)
nokogumbo (~> 1.4.1) nokogumbo (~> 1.4.1)
sass (3.4.24) sass (3.4.25)
scss_lint (0.54.0) scss_lint (0.54.0)
rake (>= 0.9, < 13) rake (>= 0.9, < 13)
sass (~> 3.4.20) sass (~> 3.4.20)
@ -450,12 +452,12 @@ GEM
sidekiq-bulk (0.1.1) sidekiq-bulk (0.1.1)
activesupport activesupport
sidekiq sidekiq
sidekiq-scheduler (2.1.8) sidekiq-scheduler (2.1.9)
redis (~> 3) redis (~> 3)
rufus-scheduler (~> 3.2) rufus-scheduler (~> 3.2)
sidekiq (>= 3) sidekiq (>= 3)
tilt (>= 1.4.0) tilt (>= 1.4.0)
sidekiq-unique-jobs (5.0.9) sidekiq-unique-jobs (5.0.10)
sidekiq (>= 4.0, <= 6.0) sidekiq (>= 4.0, <= 6.0)
thor (~> 0) thor (~> 0)
simple-navigation (4.0.5) simple-navigation (4.0.5)
@ -463,20 +465,20 @@ GEM
simple_form (3.5.0) simple_form (3.5.0)
actionpack (> 4, < 5.2) actionpack (> 4, < 5.2)
activemodel (> 4, < 5.2) activemodel (> 4, < 5.2)
simplecov (0.14.1) simplecov (0.15.1)
docile (~> 1.1.0) docile (~> 1.1.0)
json (>= 1.8, < 3) json (>= 1.8, < 3)
simplecov-html (~> 0.10.0) simplecov-html (~> 0.10.0)
simplecov-html (0.10.1) simplecov-html (0.10.2)
slop (3.6.0) slop (3.6.0)
sprockets (3.7.1) sprockets (3.7.1)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
rack (> 1, < 3) rack (> 1, < 3)
sprockets-rails (3.2.0) sprockets-rails (3.2.1)
actionpack (>= 4.0) actionpack (>= 4.0)
activesupport (>= 4.0) activesupport (>= 4.0)
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
sshkit (1.13.1) sshkit (1.14.0)
net-scp (>= 1.1.2) net-scp (>= 1.1.2)
net-ssh (>= 2.8.0) net-ssh (>= 2.8.0)
statsd-instrument (2.1.4) statsd-instrument (2.1.4)
@ -541,7 +543,7 @@ DEPENDENCIES
capistrano-yarn (~> 2.0) capistrano-yarn (~> 2.0)
capybara (~> 2.14) capybara (~> 2.14)
charlock_holmes (~> 0.7.5) charlock_holmes (~> 0.7.5)
cld3 (~> 3.1) cld3 (~> 3.2.0)
climate_control (~> 0.2) climate_control (~> 0.2)
devise (~> 4.2) devise (~> 4.2)
devise-two-factor (~> 3.0) devise-two-factor (~> 3.0)
@ -582,13 +584,13 @@ DEPENDENCIES
pghero (~> 1.7) pghero (~> 1.7)
pkg-config (~> 1.2) pkg-config (~> 1.2)
pry-rails (~> 0.3) pry-rails (~> 0.3)
puma (~> 3.8) puma (~> 3.10)
pundit (~> 1.1) pundit (~> 1.1)
rabl (~> 0.13) rabl (~> 0.13)
rack-attack (~> 5.0) rack-attack (~> 5.0)
rack-cors (~> 0.4) rack-cors (~> 0.4)
rack-timeout (~> 0.4) rack-timeout (~> 0.4)
rails (~> 5.1.0) rails (~> 5.1.4)
rails-controller-testing (~> 1.0) rails-controller-testing (~> 1.0)
rails-i18n (~> 5.0) rails-i18n (~> 5.0)
rails-settings-cached (~> 0.6) rails-settings-cached (~> 0.6)
@ -620,7 +622,7 @@ DEPENDENCIES
webpush webpush
RUBY VERSION RUBY VERSION
ruby 2.4.1p111 ruby 2.4.2p198
BUNDLED WITH BUNDLED WITH
1.15.4 1.15.4

@ -0,0 +1,34 @@
# frozen_string_literal: true
module Admin
class CustomEmojisController < BaseController
def index
@custom_emojis = CustomEmoji.where(domain: nil)
end
def new
@custom_emoji = CustomEmoji.new
end
def create
@custom_emoji = CustomEmoji.new(resource_params)
if @custom_emoji.save
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.created_msg')
else
render :new
end
end
def destroy
CustomEmoji.find(params[:id]).destroy
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.destroyed_msg')
end
private
def resource_params
params.require(:custom_emoji).permit(:shortcode, :image)
end
end
end

@ -14,8 +14,12 @@ module Admin
private private
def filtered_instances
InstanceFilter.new(filter_params).results
end
def paginated_instances def paginated_instances
Account.remote.by_domain_accounts.page(params[:page]) filtered_instances.page(params[:page])
end end
helper_method :paginated_instances helper_method :paginated_instances
@ -27,5 +31,11 @@ module Admin
def subscribeable_accounts def subscribeable_accounts
Account.with_followers.remote.where(domain: params[:by_domain]) Account.with_followers.remote.where(domain: params[:by_domain])
end end
def filter_params
params.permit(
:domain_name
)
end
end end
end end

@ -14,6 +14,7 @@ module Admin
open_deletion open_deletion
timeline_preview timeline_preview
bootstrap_timeline_accounts bootstrap_timeline_accounts
thumbnail
).freeze ).freeze
BOOLEAN_SETTINGS = %w( BOOLEAN_SETTINGS = %w(
@ -22,14 +23,23 @@ module Admin
timeline_preview timeline_preview
).freeze ).freeze
UPLOAD_SETTINGS = %w(
thumbnail
).freeze
def edit def edit
@admin_settings = Form::AdminSettings.new @admin_settings = Form::AdminSettings.new
end end
def update def update
settings_params.each do |key, value| settings_params.each do |key, value|
setting = Setting.where(var: key).first_or_initialize(var: key) if UPLOAD_SETTINGS.include?(key)
setting.update(value: value_for_update(key, value)) upload = SiteUpload.where(var: key).first_or_initialize(var: key)
upload.update(file: value)
else
setting = Setting.where(var: key).first_or_initialize(var: key)
setting.update(value: value_for_update(key, value))
end
end end
flash[:notice] = I18n.t('generic.changes_saved_msg') flash[:notice] = I18n.t('generic.changes_saved_msg')

@ -12,7 +12,30 @@ class HomeController < ApplicationController
private private
def authenticate_user! def authenticate_user!
redirect_to(single_user_mode? ? account_path(Account.first) : about_path) unless user_signed_in? return if user_signed_in?
matches = request.path.match(/\A\/web\/(statuses|accounts)\/([\d]+)\z/)
if matches
case matches[1]
when 'statuses'
status = Status.find_by(id: matches[2])
if status && (status.public_visibility? || status.unlisted_visibility?)
redirect_to(ActivityPub::TagManager.instance.url_for(status))
return
end
when 'accounts'
account = Account.find_by(id: matches[2])
if account
redirect_to(ActivityPub::TagManager.instance.url_for(account))
return
end
end
end
redirect_to(default_redirect_path)
end end
def set_initial_state_json def set_initial_state_json
@ -29,4 +52,14 @@ class HomeController < ApplicationController
admin: Account.find_local(Setting.site_contact_username), admin: Account.find_local(Setting.site_contact_username),
} }
end end
def default_redirect_path
if request.path.start_with?('/web')
new_user_session_path
elsif single_user_mode?
short_account_path(Account.first)
else
about_path
end
end
end end

@ -0,0 +1,40 @@
# frozen_string_literal: true
class MediaProxyController < ApplicationController
include RoutingHelper
def show
RedisLock.acquire(lock_options) do |lock|
if lock.acquired?
@media_attachment = MediaAttachment.remote.find(params[:id])
redownload! if @media_attachment.needs_redownload? && !reject_media?
end
end
redirect_to full_asset_url(@media_attachment.file.url(version))
end
private
def redownload!
@media_attachment.file_remote_url = @media_attachment.remote_url
@media_attachment.created_at = Time.now.utc
@media_attachment.save!
end
def version
if request.path.ends_with?('/small')
:small
else
:original
end
end
def lock_options
{ redis: Redis.current, key: "media_download:#{params[:id]}" }
end
def reject_media?
DomainBlock.find_by(domain: @media_attachment.account.domain)&.reject_media?
end
end

@ -42,4 +42,8 @@ module ApplicationHelper
content_tag(:i, nil, attributes.merge(class: class_names.join(' '))) content_tag(:i, nil, attributes.merge(class: class_names.join(' ')))
end end
def opengraph(property, content)
tag(:meta, content: content, property: property)
end
end end

@ -41,7 +41,7 @@ module SettingsHelper
end end
def filterable_languages def filterable_languages
I18n.available_locales.map { |locale| locale.to_s.split('-').first.to_sym }.uniq LanguageDetector.instance.language_names.select(&HUMAN_LOCALES.method(:key?))
end end
def hash_to_object(hash) def hash_to_object(hash)

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="61.076954mm" height="65.47831mm" viewBox="0 0 216.4144 232.00976"><path d="M211.80734 139.0875c-3.18125 16.36625-28.4925 34.2775-57.5625 37.74875-15.15875 1.80875-30.08375 3.47125-45.99875 2.74125-26.0275-1.1925-46.565-6.2125-46.565-6.2125 0 2.53375.15625 4.94625.46875 7.2025 3.38375 25.68625 25.47 27.225 46.39125 27.9425 21.11625.7225 39.91875-5.20625 39.91875-5.20625l.8675 19.09s-14.77 7.93125-41.08125 9.39c-14.50875.7975-32.52375-.365-53.50625-5.91875C9.23234 213.82 1.40609 165.31125.20859 116.09125c-.365-14.61375-.14-28.39375-.14-39.91875 0-50.33 32.97625-65.0825 32.97625-65.0825C49.67234 3.45375 78.20359.2425 107.86484 0h.72875c29.66125.2425 58.21125 3.45375 74.8375 11.09 0 0 32.975 14.7525 32.975 65.0825 0 0 .41375 37.13375-4.59875 62.915" fill="#3088d4"/><path d="M177.50984 80.077v60.94125h-24.14375v-59.15c0-12.46875-5.24625-18.7975-15.74-18.7975-11.6025 0-17.4175 7.5075-17.4175 22.3525v32.37625H96.20734V85.42325c0-14.845-5.81625-22.3525-17.41875-22.3525-10.49375 0-15.74 6.32875-15.74 18.7975v59.15H38.90484V80.077c0-12.455 3.17125-22.3525 9.54125-29.675 6.56875-7.3225 15.17125-11.07625 25.85-11.07625 12.355 0 21.71125 4.74875 27.8975 14.2475l6.01375 10.08125 6.015-10.08125c6.185-9.49875 15.54125-14.2475 27.8975-14.2475 10.6775 0 19.28 3.75375 25.85 11.07625 6.36875 7.3225 9.54 17.22 9.54 29.675" fill="#fff"/></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216.4144 232.00976"><path d="M211.80734 139.0875c-3.18125 16.36625-28.4925 34.2775-57.5625 37.74875-15.15875 1.80875-30.08375 3.47125-45.99875 2.74125-26.0275-1.1925-46.565-6.2125-46.565-6.2125 0 2.53375.15625 4.94625.46875 7.2025 3.38375 25.68625 25.47 27.225 46.39125 27.9425 21.11625.7225 39.91875-5.20625 39.91875-5.20625l.8675 19.09s-14.77 7.93125-41.08125 9.39c-14.50875.7975-32.52375-.365-53.50625-5.91875C9.23234 213.82 1.40609 165.31125.20859 116.09125c-.365-14.61375-.14-28.39375-.14-39.91875 0-50.33 32.97625-65.0825 32.97625-65.0825C49.67234 3.45375 78.20359.2425 107.86484 0h.72875c29.66125.2425 58.21125 3.45375 74.8375 11.09 0 0 32.975 14.7525 32.975 65.0825 0 0 .41375 37.13375-4.59875 62.915" fill="#3088d4"/><path d="M177.50984 80.077v60.94125h-24.14375v-59.15c0-12.46875-5.24625-18.7975-15.74-18.7975-11.6025 0-17.4175 7.5075-17.4175 22.3525v32.37625H96.20734V85.42325c0-14.845-5.81625-22.3525-17.41875-22.3525-10.49375 0-15.74 6.32875-15.74 18.7975v59.15H38.90484V80.077c0-12.455 3.17125-22.3525 9.54125-29.675 6.56875-7.3225 15.17125-11.07625 25.85-11.07625 12.355 0 21.71125 4.74875 27.8975 14.2475l6.01375 10.08125 6.015-10.08125c6.185-9.49875 15.54125-14.2475 27.8975-14.2475 10.6775 0 19.28 3.75375 25.85 11.07625 6.36875 7.3225 9.54 17.22 9.54 29.675" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="61.077141mm" height="65.47831mm" viewBox="0 0 216.41507 232.00976"><path d="M211.80683 139.0875c-3.1825 16.36625-28.4925 34.2775-57.5625 37.74875-15.16 1.80875-30.0825 3.47125-45.99875 2.74125-26.0275-1.1925-46.565-6.2125-46.565-6.2125 0 2.53375.15625 4.94625.46875 7.2025 3.38375 25.68625 25.47 27.225 46.3925 27.9425 21.115.7225 39.91625-5.20625 39.91625-5.20625l.86875 19.09s-14.77 7.93125-41.08125 9.39c-14.50875.7975-32.52375-.365-53.50625-5.91875C9.23183 213.82 1.40558 165.31125.20808 116.09125c-.36375-14.61375-.14-28.39375-.14-39.91875 0-50.33 32.97625-65.0825 32.97625-65.0825C49.67058 3.45375 78.20308.2425 107.86433 0h.72875c29.66125.2425 58.21125 3.45375 74.8375 11.09 0 0 32.97625 14.7525 32.97625 65.0825 0 0 .4125 37.13375-4.6 62.915" fill="#3088d4"/><path d="M65.68743 96.45938c0 9.01375-7.3075 16.32125-16.3225 16.32125-9.01375 0-16.32-7.3075-16.32-16.32125 0-9.01375 7.30625-16.3225 16.32-16.3225 9.015 0 16.3225 7.30875 16.3225 16.3225M124.52893 96.45938c0 9.01375-7.30875 16.32125-16.3225 16.32125-9.01375 0-16.32125-7.3075-16.32125-16.32125 0-9.01375 7.3075-16.3225 16.32125-16.3225 9.01375 0 16.3225 7.30875 16.3225 16.3225M183.36933 96.45938c0 9.01375-7.3075 16.32125-16.32125 16.32125-9.01375 0-16.32125-7.3075-16.32125-16.32125 0-9.01375 7.3075-16.3225 16.32125-16.3225 9.01375 0 16.32125 7.30875 16.32125 16.3225" fill="#fff"/></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216.41507 232.00976"><path d="M211.80683 139.0875c-3.1825 16.36625-28.4925 34.2775-57.5625 37.74875-15.16 1.80875-30.0825 3.47125-45.99875 2.74125-26.0275-1.1925-46.565-6.2125-46.565-6.2125 0 2.53375.15625 4.94625.46875 7.2025 3.38375 25.68625 25.47 27.225 46.3925 27.9425 21.115.7225 39.91625-5.20625 39.91625-5.20625l.86875 19.09s-14.77 7.93125-41.08125 9.39c-14.50875.7975-32.52375-.365-53.50625-5.91875C9.23183 213.82 1.40558 165.31125.20808 116.09125c-.36375-14.61375-.14-28.39375-.14-39.91875 0-50.33 32.97625-65.0825 32.97625-65.0825C49.67058 3.45375 78.20308.2425 107.86433 0h.72875c29.66125.2425 58.21125 3.45375 74.8375 11.09 0 0 32.97625 14.7525 32.97625 65.0825 0 0 .4125 37.13375-4.6 62.915" fill="#3088d4"/><path d="M65.68743 96.45938c0 9.01375-7.3075 16.32125-16.3225 16.32125-9.01375 0-16.32-7.3075-16.32-16.32125 0-9.01375 7.30625-16.3225 16.32-16.3225 9.015 0 16.3225 7.30875 16.3225 16.3225M124.52893 96.45938c0 9.01375-7.30875 16.32125-16.3225 16.32125-9.01375 0-16.32125-7.3075-16.32125-16.32125 0-9.01375 7.3075-16.3225 16.32125-16.3225 9.01375 0 16.3225 7.30875 16.3225 16.3225M183.36933 96.45938c0 9.01375-7.3075 16.32125-16.32125 16.32125-9.01375 0-16.32125-7.3075-16.32125-16.32125 0-9.01375 7.3075-16.3225 16.32125-16.3225 9.01375 0 16.32125 7.30875 16.32125 16.3225" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

@ -0,0 +1,17 @@
export const HEIGHT_CACHE_SET = 'HEIGHT_CACHE_SET';
export const HEIGHT_CACHE_CLEAR = 'HEIGHT_CACHE_CLEAR';
export function setHeight (key, id, height) {
return {
type: HEIGHT_CACHE_SET,
key,
id,
height,
};
};
export function clearHeight () {
return {
type: HEIGHT_CACHE_CLEAR,
};
};

@ -23,9 +23,6 @@ export const STATUS_UNMUTE_REQUEST = 'STATUS_UNMUTE_REQUEST';
export const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS'; export const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS';
export const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL'; export const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL';
export const STATUS_SET_HEIGHT = 'STATUS_SET_HEIGHT';
export const STATUSES_CLEAR_HEIGHT = 'STATUSES_CLEAR_HEIGHT';
export function fetchStatusRequest(id, skipLoading) { export function fetchStatusRequest(id, skipLoading) {
return { return {
type: STATUS_FETCH_REQUEST, type: STATUS_FETCH_REQUEST,
@ -218,17 +215,3 @@ export function unmuteStatusFail(id, error) {
error, error,
}; };
}; };
export function setStatusHeight (id, height) {
return {
type: STATUS_SET_HEIGHT,
id,
height,
};
};
export function clearStatusesHeight () {
return {
type: STATUSES_CLEAR_HEIGHT,
};
};

@ -7,10 +7,13 @@ import getRectFromEntry from '../features/ui/util/get_rect_from_entry';
export default class IntersectionObserverArticle extends ImmutablePureComponent { export default class IntersectionObserverArticle extends ImmutablePureComponent {
static propTypes = { static propTypes = {
intersectionObserverWrapper: PropTypes.object, intersectionObserverWrapper: PropTypes.object.isRequired,
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
saveHeightKey: PropTypes.string,
cachedHeight: PropTypes.number,
onHeightChange: PropTypes.func,
children: PropTypes.node, children: PropTypes.node,
}; };
@ -34,13 +37,10 @@ export default class IntersectionObserverArticle extends ImmutablePureComponent
} }
componentDidMount () { componentDidMount () {
if (!this.props.intersectionObserverWrapper) { const { intersectionObserverWrapper, id } = this.props;
// TODO: enable IntersectionObserver optimization for notification statuses.
// These are managed in notifications/index.js rather than status_list.js intersectionObserverWrapper.observe(
return; id,
}
this.props.intersectionObserverWrapper.observe(
this.props.id,
this.node, this.node,
this.handleIntersection this.handleIntersection
); );
@ -49,20 +49,21 @@ export default class IntersectionObserverArticle extends ImmutablePureComponent
} }
componentWillUnmount () { componentWillUnmount () {
if (this.props.intersectionObserverWrapper) { const { intersectionObserverWrapper, id } = this.props;
this.props.intersectionObserverWrapper.unobserve(this.props.id, this.node); intersectionObserverWrapper.unobserve(id, this.node);
}
this.componentMounted = false; this.componentMounted = false;
} }
handleIntersection = (entry) => { handleIntersection = (entry) => {
const { onHeightChange, saveHeightKey, id } = this.props;
if (this.node && this.node.children.length !== 0) { if (this.node && this.node.children.length !== 0) {
// save the height of the fully-rendered element // save the height of the fully-rendered element
this.height = getRectFromEntry(entry).height; this.height = getRectFromEntry(entry).height;
if (this.props.onHeightChange) { if (onHeightChange && saveHeightKey) {
this.props.onHeightChange(this.props.status, this.height); onHeightChange(saveHeightKey, id, this.height);
} }
} }
@ -94,16 +95,16 @@ export default class IntersectionObserverArticle extends ImmutablePureComponent
} }
render () { render () {
const { children, id, index, listLength } = this.props; const { children, id, index, listLength, cachedHeight } = this.props;
const { isIntersecting, isHidden } = this.state; const { isIntersecting, isHidden } = this.state;
if (!isIntersecting && isHidden) { if (!isIntersecting && (isHidden || cachedHeight)) {
return ( return (
<article <article
ref={this.handleRef} ref={this.handleRef}
aria-posinset={index} aria-posinset={index}
aria-setsize={listLength} aria-setsize={listLength}
style={{ height: `${this.height}px`, opacity: 0, overflow: 'hidden' }} style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }}
data-id={id} data-id={id}
tabIndex='0' tabIndex='0'
> >

@ -17,7 +17,7 @@ export default class LoadMore extends React.PureComponent {
const { visible } = this.props; const { visible } = this.props;
return ( return (
<button className='load-more' disabled={!visible} style={{ opacity: visible ? 1 : 0 }} onClick={this.props.onClick}> <button className='load-more' disabled={!visible} style={{ visibility: visible ? 'visible' : 'hidden' }} onClick={this.props.onClick}>
<FormattedMessage id='status.load_more' defaultMessage='Load more' /> <FormattedMessage id='status.load_more' defaultMessage='Load more' />
</button> </button>
); );

@ -122,8 +122,8 @@ class Item extends React.PureComponent {
const hasSize = typeof originalWidth === 'number' && typeof previewWidth === 'number'; const hasSize = typeof originalWidth === 'number' && typeof previewWidth === 'number';
const srcSet = hasSize && `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w`; const srcSet = hasSize ? `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w` : null;
const sizes = hasSize && `(min-width: 1025px) ${320 * (width / 100)}px, ${width}vw`; const sizes = hasSize ? `(min-width: 1025px) ${320 * (width / 100)}px, ${width}vw` : null;
thumbnail = ( thumbnail = (
<a <a

@ -1,7 +1,7 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { ScrollContainer } from 'react-router-scroll'; import { ScrollContainer } from 'react-router-scroll';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import IntersectionObserverArticle from './intersection_observer_article'; import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
import LoadMore from './load_more'; import LoadMore from './load_more';
import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper'; import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
@ -9,6 +9,10 @@ import { List as ImmutableList } from 'immutable';
export default class ScrollableList extends PureComponent { export default class ScrollableList extends PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = { static propTypes = {
scrollKey: PropTypes.string.isRequired, scrollKey: PropTypes.string.isRequired,
onScrollToBottom: PropTypes.func, onScrollToBottom: PropTypes.func,
@ -163,7 +167,7 @@ export default class ScrollableList extends PureComponent {
const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage } = this.props; const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage } = this.props;
const childrenCount = React.Children.count(children); const childrenCount = React.Children.count(children);
const loadMore = <LoadMore visible={!isLoading && childrenCount > 0 && hasMore} onClick={this.handleLoadMore} />; const loadMore = (hasMore && childrenCount > 0) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null;
let scrollableArea = null; let scrollableArea = null;
if (isLoading || childrenCount > 0 || !emptyMessage) { if (isLoading || childrenCount > 0 || !emptyMessage) {
@ -173,9 +177,16 @@ export default class ScrollableList extends PureComponent {
{prepend} {prepend}
{React.Children.map(this.props.children, (child, index) => ( {React.Children.map(this.props.children, (child, index) => (
<IntersectionObserverArticle key={child.key} id={child.key} index={index} listLength={childrenCount} intersectionObserverWrapper={this.intersectionObserverWrapper}> <IntersectionObserverArticleContainer
key={child.key}
id={child.key}
index={index}
listLength={childrenCount}
intersectionObserverWrapper={this.intersectionObserverWrapper}
saveHeightKey={trackScroll ? `${this.context.router.route.location.key}:${scrollKey}` : null}
>
{child} {child}
</IntersectionObserverArticle> </IntersectionObserverArticleContainer>
))} ))}
{loadMore} {loadMore}

@ -12,7 +12,7 @@ import StatusContent from './status_content';
import StatusActionBar from './status_action_bar'; import StatusActionBar from './status_action_bar';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { MediaGallery, VideoPlayer } from '../features/ui/util/async-components'; import { MediaGallery, Video } from '../features/ui/util/async-components';
// We use the component (and not the container) since we do not want // We use the component (and not the container) since we do not want
// to use the progress bar to show download progress // to use the progress bar to show download progress
@ -91,6 +91,10 @@ export default class Status extends ImmutablePureComponent {
return <div className='media-spoiler-video' style={{ height: '110px' }} />; return <div className='media-spoiler-video' style={{ height: '110px' }} />;
} }
handleOpenVideo = startTime => {
this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), startTime);
}
render () { render () {
let media = null; let media = null;
let statusAvatar; let statusAvatar;
@ -130,9 +134,18 @@ export default class Status extends ImmutablePureComponent {
if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) { if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
const video = status.getIn(['media_attachments', 0]);
media = ( media = (
<Bundle fetchComponent={VideoPlayer} loading={this.renderLoadingVideoPlayer} > <Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
{Component => <Component media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} onOpenVideo={this.props.onOpenVideo} />} {Component => <Component
preview={video.get('preview_url')}
src={video.get('url')}
width={239}
height={110}
sensitive={status.get('sensitive')}
onOpenVideo={this.handleOpenVideo}
/>}
</Bundle> </Bundle>
); );
} else { } else {

@ -0,0 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';
import Card from '../features/status/components/card';
import { fromJS } from 'immutable';
export default class CardContainer extends React.PureComponent {
static propTypes = {
locale: PropTypes.string,
card: PropTypes.array.isRequired,
};
render () {
const { card, ...props } = this.props;
return <Card card={fromJS(card)} {...props} />;
}
}

@ -0,0 +1,17 @@
import { connect } from 'react-redux';
import IntersectionObserverArticle from '../components/intersection_observer_article';
import { setHeight } from '../actions/height_cache';
const makeMapStateToProps = (state, props) => ({
cachedHeight: state.getIn(['height_cache', props.saveHeightKey, props.id]),
});
const mapDispatchToProps = (dispatch) => ({
onHeightChange (key, id, height) {
dispatch(setHeight(key, id, height));
},
});
export default connect(makeMapStateToProps, mapDispatchToProps)(IntersectionObserverArticle);

@ -0,0 +1,34 @@
import React from 'react';
import PropTypes from 'prop-types';
import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales';
import MediaGallery from '../components/media_gallery';
import { fromJS } from 'immutable';
const { localeData, messages } = getLocale();
addLocaleData(localeData);
export default class MediaGalleryContainer extends React.PureComponent {
static propTypes = {
locale: PropTypes.string.isRequired,
media: PropTypes.array.isRequired,
};
handleOpenMedia = () => {}
render () {
const { locale, media, ...props } = this.props;
return (
<IntlProvider locale={locale} messages={messages}>
<MediaGallery
{...props}
media={fromJS(media)}
onOpenMedia={this.handleOpenMedia}
/>
</IntlProvider>
);
}
}

@ -21,7 +21,7 @@ import {
blockAccount, blockAccount,
muteAccount, muteAccount,
} from '../actions/accounts'; } from '../actions/accounts';
import { muteStatus, unmuteStatus, deleteStatus, setStatusHeight } from '../actions/statuses'; import { muteStatus, unmuteStatus, deleteStatus } from '../actions/statuses';
import { initReport } from '../actions/reports'; import { initReport } from '../actions/reports';
import { openModal } from '../actions/modal'; import { openModal } from '../actions/modal';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
@ -141,10 +141,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
} }
}, },
onHeightChange (status, height) {
dispatch(setStatusHeight(status.get('id'), height));
},
}); });
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status)); export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));

@ -0,0 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales';
import Video from '../features/video';
const { localeData, messages } = getLocale();
addLocaleData(localeData);
export default class VideoContainer extends React.PureComponent {
static propTypes = {
locale: PropTypes.string.isRequired,
};
render () {
const { locale, ...props } = this.props;
return (
<IntlProvider locale={locale} messages={messages}>
<Video {...props} />
</IntlProvider>
);
}
}

@ -3,28 +3,48 @@ import Trie from 'substring-trie';
const trie = new Trie(Object.keys(unicodeMapping)); const trie = new Trie(Object.keys(unicodeMapping));
const emojify = str => { const emojify = (str, customEmojis = {}) => {
let rtn = ''; // This walks through the string from start to end, ignoring any tags (<p>, <br>, etc.)
for (;;) { // and replacing valid unicode strings
let match, i = 0; // that _aren't_ within tags with an <img> version.
while (i < str.length && str[i] !== '<' && !(match = trie.search(str.slice(i)))) { // The goal is to be the same as an emojione.regUnicode replacement, but faster.
i += str.codePointAt(i) < 65536 ? 1 : 2; let i = -1;
} let insideTag = false;
if (i === str.length) let insideShortname = false;
break; let shortnameStartIndex = -1;
else if (str[i] === '<') { let match;
let tagend = str.indexOf('>', i + 1) + 1; while (++i < str.length) {
if (!tagend) const char = str.charAt(i);
break; if (insideShortname && char === ':') {
rtn += str.slice(0, tagend); const shortname = str.substring(shortnameStartIndex, i + 1);
str = str.slice(tagend); if (shortname in customEmojis) {
} else { const replacement = `<img draggable="false" class="emojione" alt="${shortname}" title="${shortname}" src="${customEmojis[shortname]}" />`;
const [filename, shortCode] = unicodeMapping[match]; str = str.substring(0, shortnameStartIndex) + replacement + str.substring(i + 1);
rtn += str.slice(0, i) + `<img draggable="false" class="emojione" alt="${match}" title=":${shortCode}:" src="/emoji/${filename}.svg" />`; i += (replacement.length - shortname.length - 1); // jump ahead the length we've added to the string
str = str.slice(i + match.length); } else {
i--;
}
insideShortname = false;
} else if (insideTag && char === '>') {
insideTag = false;
} else if (char === '<') {
insideTag = true;
insideShortname = false;
} else if (!insideTag && char === ':') {
insideShortname = true;
shortnameStartIndex = i;
} else if (!insideTag && (match = trie.search(str.substring(i)))) {
const unicodeStr = match;
if (unicodeStr in unicodeMapping) {
const [filename, shortCode] = unicodeMapping[unicodeStr];
const alt = unicodeStr;
const replacement = `<img draggable="false" class="emojione" alt="${alt}" title=":${shortCode}:" src="/emoji/${filename}.svg" />`;
str = str.substring(0, i) + replacement + str.substring(i + unicodeStr.length);
i += (replacement.length - unicodeStr.length); // jump ahead the length we've added to the string
}
} }
} }
return rtn + str; return str;
}; };
export default emojify; export default emojify;

@ -1,7 +1,9 @@
import { urlRegex } from './url_regex';
const urlPlaceholder = 'xxxxxxxxxxxxxxxxxxxxxxx'; const urlPlaceholder = 'xxxxxxxxxxxxxxxxxxxxxxx';
export function countableText(inputText) { export function countableText(inputText) {
return inputText return inputText
.replace(/https?:\/\/\S+/g, urlPlaceholder) .replace(urlRegex, urlPlaceholder)
.replace(/(?:^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+)/ig, '@$2'); .replace(/(?:^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+)/ig, '@$2');
}; };

@ -0,0 +1,196 @@
const regexen = {};
const regexSupplant = function(regex, flags) {
flags = flags || '';
if (typeof regex !== 'string') {
if (regex.global && flags.indexOf('g') < 0) {
flags += 'g';
}
if (regex.ignoreCase && flags.indexOf('i') < 0) {
flags += 'i';
}
if (regex.multiline && flags.indexOf('m') < 0) {
flags += 'm';
}
regex = regex.source;
}
return new RegExp(regex.replace(/#\{(\w+)\}/g, function(match, name) {
var newRegex = regexen[name] || '';
if (typeof newRegex !== 'string') {
newRegex = newRegex.source;
}
return newRegex;
}), flags);
};
const stringSupplant = function(str, values) {
return str.replace(/#\{(\w+)\}/g, function(match, name) {
return values[name] || '';
});
};
export const urlRegex = (function() {
regexen.spaces_group = /\x09-\x0D\x20\x85\xA0\u1680\u180E\u2000-\u200A\u2028\u2029\u202F\u205F\u3000/;
regexen.invalid_chars_group = /\uFFFE\uFEFF\uFFFF\u202A-\u202E/;
regexen.punct = /\!'#%&'\(\)*\+,\\\-\.\/:;<=>\?@\[\]\^_{|}~\$/;
regexen.validUrlPrecedingChars = regexSupplant(/(?:[^A-Za-z0-9@$##{invalid_chars_group}]|^)/);
regexen.invalidDomainChars = stringSupplant('#{punct}#{spaces_group}#{invalid_chars_group}', regexen);
regexen.validDomainChars = regexSupplant(/[^#{invalidDomainChars}]/);
regexen.validSubdomain = regexSupplant(/(?:(?:#{validDomainChars}(?:[_-]|#{validDomainChars})*)?#{validDomainChars}\.)/);
regexen.validDomainName = regexSupplant(/(?:(?:#{validDomainChars}(?:-|#{validDomainChars})*)?#{validDomainChars}\.)/);
regexen.validGTLD = regexSupplant(RegExp(
'(?:(?:' +
'삼성|닷컴|닷넷|香格里拉|餐厅|食品|飞利浦|電訊盈科|集团|通販|购物|谷歌|诺基亚|联通|网络|网站|网店|网址|组织机构|移动|珠宝|点看|游戏|淡马锡|机构|書籍|时尚|新闻|政府|' +
'政务|手表|手机|我爱你|慈善|微博|广东|工行|家電|娱乐|天主教|大拿|大众汽车|在线|嘉里大酒店|嘉里|商标|商店|商城|公益|公司|八卦|健康|信息|佛山|企业|中文网|中信|世界|' +
'ポイント|ファッション|セール|ストア|コム|グーグル|クラウド|みんな|คอม|संगठन|नेट|कॉम|همراه|موقع|موبايلي|كوم|كاثوليك|عرب|شبكة|' +
'بيتك|بازار|العليان|ارامكو|اتصالات|ابوظبي|קום|сайт|рус|орг|онлайн|москва|ком|католик|дети|' +
'zuerich|zone|zippo|zip|zero|zara|zappos|yun|youtube|you|yokohama|yoga|yodobashi|yandex|yamaxun|' +
'yahoo|yachts|xyz|xxx|xperia|xin|xihuan|xfinity|xerox|xbox|wtf|wtc|wow|world|works|work|woodside|' +
'wolterskluwer|wme|winners|wine|windows|win|williamhill|wiki|wien|whoswho|weir|weibo|wedding|wed|' +
'website|weber|webcam|weatherchannel|weather|watches|watch|warman|wanggou|wang|walter|walmart|' +
'wales|vuelos|voyage|voto|voting|vote|volvo|volkswagen|vodka|vlaanderen|vivo|viva|vistaprint|' +
'vista|vision|visa|virgin|vip|vin|villas|viking|vig|video|viajes|vet|versicherung|' +
'vermögensberatung|vermögensberater|verisign|ventures|vegas|vanguard|vana|vacations|ups|uol|uno|' +
'university|unicom|uconnect|ubs|ubank|tvs|tushu|tunes|tui|tube|trv|trust|travelersinsurance|' +
'travelers|travelchannel|travel|training|trading|trade|toys|toyota|town|tours|total|toshiba|' +
'toray|top|tools|tokyo|today|tmall|tkmaxx|tjx|tjmaxx|tirol|tires|tips|tiffany|tienda|tickets|' +
'tiaa|theatre|theater|thd|teva|tennis|temasek|telefonica|telecity|tel|technology|tech|team|tdk|' +
'tci|taxi|tax|tattoo|tatar|tatamotors|target|taobao|talk|taipei|tab|systems|symantec|sydney|' +
'swiss|swiftcover|swatch|suzuki|surgery|surf|support|supply|supplies|sucks|style|study|studio|' +
'stream|store|storage|stockholm|stcgroup|stc|statoil|statefarm|statebank|starhub|star|staples|' +
'stada|srt|srl|spreadbetting|spot|spiegel|space|soy|sony|song|solutions|solar|sohu|software|' +
'softbank|social|soccer|sncf|smile|smart|sling|skype|sky|skin|ski|site|singles|sina|silk|shriram|' +
'showtime|show|shouji|shopping|shop|shoes|shiksha|shia|shell|shaw|sharp|shangrila|sfr|sexy|sex|' +
'sew|seven|ses|services|sener|select|seek|security|secure|seat|search|scot|scor|scjohnson|' +
'science|schwarz|schule|school|scholarships|schmidt|schaeffler|scb|sca|sbs|sbi|saxo|save|sas|' +
'sarl|sapo|sap|sanofi|sandvikcoromant|sandvik|samsung|samsclub|salon|sale|sakura|safety|safe|' +
'saarland|ryukyu|rwe|run|ruhr|rugby|rsvp|room|rogers|rodeo|rocks|rocher|rmit|rip|rio|ril|' +
'rightathome|ricoh|richardli|rich|rexroth|reviews|review|restaurant|rest|republican|report|' +
'repair|rentals|rent|ren|reliance|reit|reisen|reise|rehab|redumbrella|redstone|red|recipes|' +
'realty|realtor|realestate|read|raid|radio|racing|qvc|quest|quebec|qpon|pwc|pub|prudential|pru|' +
'protection|property|properties|promo|progressive|prof|productions|prod|pro|prime|press|praxi|' +
'pramerica|post|porn|politie|poker|pohl|pnc|plus|plumbing|playstation|play|place|pizza|pioneer|' +
'pink|ping|pin|pid|pictures|pictet|pics|piaget|physio|photos|photography|photo|phone|philips|phd|' +
'pharmacy|pfizer|pet|pccw|pay|passagens|party|parts|partners|pars|paris|panerai|panasonic|' +
'pamperedchef|page|ovh|ott|otsuka|osaka|origins|orientexpress|organic|org|orange|oracle|open|ooo|' +
'onyourside|online|onl|ong|one|omega|ollo|oldnavy|olayangroup|olayan|okinawa|office|off|observer|' +
'obi|nyc|ntt|nrw|nra|nowtv|nowruz|now|norton|northwesternmutual|nokia|nissay|nissan|ninja|nikon|' +
'nike|nico|nhk|ngo|nfl|nexus|nextdirect|next|news|newholland|new|neustar|network|netflix|netbank|' +
'net|nec|nba|navy|natura|nationwide|name|nagoya|nadex|nab|mutuelle|mutual|museum|mtr|mtpc|mtn|' +
'msd|movistar|movie|mov|motorcycles|moto|moscow|mortgage|mormon|mopar|montblanc|monster|money|' +
'monash|mom|moi|moe|moda|mobily|mobile|mobi|mma|mls|mlb|mitsubishi|mit|mint|mini|mil|microsoft|' +
'miami|metlife|merckmsd|meo|menu|men|memorial|meme|melbourne|meet|media|med|mckinsey|mcdonalds|' +
'mcd|mba|mattel|maserati|marshalls|marriott|markets|marketing|market|map|mango|management|man|' +
'makeup|maison|maif|madrid|macys|luxury|luxe|lupin|lundbeck|ltda|ltd|lplfinancial|lpl|love|lotto|' +
'lotte|london|lol|loft|locus|locker|loans|loan|lixil|living|live|lipsy|link|linde|lincoln|limo|' +
'limited|lilly|like|lighting|lifestyle|lifeinsurance|life|lidl|liaison|lgbt|lexus|lego|legal|' +
'lefrak|leclerc|lease|lds|lawyer|law|latrobe|latino|lat|lasalle|lanxess|landrover|land|lancome|' +
'lancia|lancaster|lamer|lamborghini|ladbrokes|lacaixa|kyoto|kuokgroup|kred|krd|kpn|kpmg|kosher|' +
'komatsu|koeln|kiwi|kitchen|kindle|kinder|kim|kia|kfh|kerryproperties|kerrylogistics|kerryhotels|' +
'kddi|kaufen|juniper|juegos|jprs|jpmorgan|joy|jot|joburg|jobs|jnj|jmp|jll|jlc|jio|jewelry|jetzt|' +
'jeep|jcp|jcb|java|jaguar|iwc|iveco|itv|itau|istanbul|ist|ismaili|iselect|irish|ipiranga|' +
'investments|intuit|international|intel|int|insure|insurance|institute|ink|ing|info|infiniti|' +
'industries|immobilien|immo|imdb|imamat|ikano|iinet|ifm|ieee|icu|ice|icbc|ibm|hyundai|hyatt|' +
'hughes|htc|hsbc|how|house|hotmail|hotels|hoteles|hot|hosting|host|hospital|horse|honeywell|' +
'honda|homesense|homes|homegoods|homedepot|holiday|holdings|hockey|hkt|hiv|hitachi|hisamitsu|' +
'hiphop|hgtv|hermes|here|helsinki|help|healthcare|health|hdfcbank|hdfc|hbo|haus|hangout|hamburg|' +
'hair|guru|guitars|guide|guge|gucci|guardian|group|grocery|gripe|green|gratis|graphics|grainger|' +
'gov|got|gop|google|goog|goodyear|goodhands|goo|golf|goldpoint|gold|godaddy|gmx|gmo|gmbh|gmail|' +
'globo|global|gle|glass|glade|giving|gives|gifts|gift|ggee|george|genting|gent|gea|gdn|gbiz|' +
'garden|gap|games|game|gallup|gallo|gallery|gal|fyi|futbol|furniture|fund|fun|fujixerox|fujitsu|' +
'ftr|frontier|frontdoor|frogans|frl|fresenius|free|fox|foundation|forum|forsale|forex|ford|' +
'football|foodnetwork|food|foo|fly|flsmidth|flowers|florist|flir|flights|flickr|fitness|fit|' +
'fishing|fish|firmdale|firestone|fire|financial|finance|final|film|fido|fidelity|fiat|ferrero|' +
'ferrari|feedback|fedex|fast|fashion|farmers|farm|fans|fan|family|faith|fairwinds|fail|fage|' +
'extraspace|express|exposed|expert|exchange|everbank|events|eus|eurovision|etisalat|esurance|' +
'estate|esq|erni|ericsson|equipment|epson|epost|enterprises|engineering|engineer|energy|emerck|' +
'email|education|edu|edeka|eco|eat|earth|dvr|dvag|durban|dupont|duns|dunlop|duck|dubai|dtv|drive|' +
'download|dot|doosan|domains|doha|dog|dodge|doctor|docs|dnp|diy|dish|discover|discount|directory|' +
'direct|digital|diet|diamonds|dhl|dev|design|desi|dentist|dental|democrat|delta|deloitte|dell|' +
'delivery|degree|deals|dealer|deal|dds|dclk|day|datsun|dating|date|data|dance|dad|dabur|cyou|' +
'cymru|cuisinella|csc|cruises|cruise|crs|crown|cricket|creditunion|creditcard|credit|courses|' +
'coupons|coupon|country|corsica|coop|cool|cookingchannel|cooking|contractors|contact|consulting|' +
'construction|condos|comsec|computer|compare|company|community|commbank|comcast|com|cologne|' +
'college|coffee|codes|coach|clubmed|club|cloud|clothing|clinique|clinic|click|cleaning|claims|' +
'cityeats|city|citic|citi|citadel|cisco|circle|cipriani|church|chrysler|chrome|christmas|chloe|' +
'chintai|cheap|chat|chase|channel|chanel|cfd|cfa|cern|ceo|center|ceb|cbs|cbre|cbn|cba|catholic|' +
'catering|cat|casino|cash|caseih|case|casa|cartier|cars|careers|career|care|cards|caravan|car|' +
'capitalone|capital|capetown|canon|cancerresearch|camp|camera|cam|calvinklein|call|cal|cafe|cab|' +
'bzh|buzz|buy|business|builders|build|bugatti|budapest|brussels|brother|broker|broadway|' +
'bridgestone|bradesco|box|boutique|bot|boston|bostik|bosch|boots|booking|book|boo|bond|bom|bofa|' +
'boehringer|boats|bnpparibas|bnl|bmw|bms|blue|bloomberg|blog|blockbuster|blanco|blackfriday|' +
'black|biz|bio|bingo|bing|bike|bid|bible|bharti|bet|bestbuy|best|berlin|bentley|beer|beauty|' +
'beats|bcn|bcg|bbva|bbt|bbc|bayern|bauhaus|basketball|baseball|bargains|barefoot|barclays|' +
'barclaycard|barcelona|bar|bank|band|bananarepublic|banamex|baidu|baby|azure|axa|aws|avianca|' +
'autos|auto|author|auspost|audio|audible|audi|auction|attorney|athleta|associates|asia|asda|arte|' +
'art|arpa|army|archi|aramco|arab|aquarelle|apple|app|apartments|aol|anz|anquan|android|analytics|' +
'amsterdam|amica|amfam|amex|americanfamily|americanexpress|alstom|alsace|ally|allstate|allfinanz|' +
'alipay|alibaba|alfaromeo|akdn|airtel|airforce|airbus|aigo|aig|agency|agakhan|africa|afl|' +
'afamilycompany|aetna|aero|aeg|adult|ads|adac|actor|active|aco|accountants|accountant|accenture|' +
'academy|abudhabi|abogado|able|abc|abbvie|abbott|abb|abarth|aarp|aaa|onion' +
')(?=[^0-9a-zA-Z@]|$))'));
regexen.validCCTLD = regexSupplant(RegExp(
'(?:(?:' +
'한국|香港|澳門|新加坡|台灣|台湾|中國|中国|გე|ไทย|ලංකා|ഭാരതം|ಭಾರತ|భారత్|சிங்கப்பூர்|இலங்கை|இந்தியா|ଭାରତ|ભારત|ਭਾਰਤ|' +
'ভাৰত|ভারত|বাংলা|भारोत|भारतम्|भारत|ڀارت|پاکستان|مليسيا|مصر|قطر|فلسطين|عمان|عراق|سورية|سودان|تونس|' +
'بھارت|بارت|ایران|امارات|المغرب|السعودية|الجزائر|الاردن|հայ|қаз|укр|срб|рф|мон|мкд|ею|бел|бг|ελ|' +
'zw|zm|za|yt|ye|ws|wf|vu|vn|vi|vg|ve|vc|va|uz|uy|us|um|uk|ug|ua|tz|tw|tv|tt|tr|tp|to|tn|tm|tl|tk|' +
'tj|th|tg|tf|td|tc|sz|sy|sx|sv|su|st|ss|sr|so|sn|sm|sl|sk|sj|si|sh|sg|se|sd|sc|sb|sa|rw|ru|rs|ro|' +
're|qa|py|pw|pt|ps|pr|pn|pm|pl|pk|ph|pg|pf|pe|pa|om|nz|nu|nr|np|no|nl|ni|ng|nf|ne|nc|na|mz|my|mx|' +
'mw|mv|mu|mt|ms|mr|mq|mp|mo|mn|mm|ml|mk|mh|mg|mf|me|md|mc|ma|ly|lv|lu|lt|ls|lr|lk|li|lc|lb|la|kz|' +
'ky|kw|kr|kp|kn|km|ki|kh|kg|ke|jp|jo|jm|je|it|is|ir|iq|io|in|im|il|ie|id|hu|ht|hr|hn|hm|hk|gy|gw|' +
'gu|gt|gs|gr|gq|gp|gn|gm|gl|gi|gh|gg|gf|ge|gd|gb|ga|fr|fo|fm|fk|fj|fi|eu|et|es|er|eh|eg|ee|ec|dz|' +
'do|dm|dk|dj|de|cz|cy|cx|cw|cv|cu|cr|co|cn|cm|cl|ck|ci|ch|cg|cf|cd|cc|ca|bz|by|bw|bv|bt|bs|br|bq|' +
'bo|bn|bm|bl|bj|bi|bh|bg|bf|be|bd|bb|ba|az|ax|aw|au|at|as|ar|aq|ao|an|am|al|ai|ag|af|ae|ad|ac' +
')(?=[^0-9a-zA-Z@]|$))'));
regexen.validPunycode = /(?:xn--[0-9a-z]+)/;
regexen.validSpecialCCTLD = /(?:(?:co|tv)(?=[^0-9a-zA-Z@]|$))/;
regexen.validDomain = regexSupplant(/(?:#{validSubdomain}*#{validDomainName}(?:#{validGTLD}|#{validCCTLD}|#{validPunycode}))/);
regexen.validPortNumber = /[0-9]+/;
regexen.pd = /\u002d\u058a\u05be\u1400\u1806\u2010-\u2015\u2e17\u2e1a\u2e3a\u2e40\u301c\u3030\u30a0\ufe31\ufe58\ufe63\uff0d/;
regexen.validGeneralUrlPathChars = regexSupplant(/[^#{spaces_group}\(\)\?]/i);
// Allow URL paths to contain up to two nested levels of balanced parens
// 1. Used in Wikipedia URLs like /Primer_(film)
// 2. Used in IIS sessions like /S(dfd346)/
// 3. Used in Rdio URLs like /track/We_Up_(Album_Version_(Edited))/
regexen.validUrlBalancedParens = regexSupplant(
'\\(' +
'(?:' +
'#{validGeneralUrlPathChars}+' +
'|' +
// allow one nested level of balanced parentheses
'(?:' +
'#{validGeneralUrlPathChars}*' +
'\\(' +
'#{validGeneralUrlPathChars}+' +
'\\)' +
'#{validGeneralUrlPathChars}*' +
')' +
')' +
'\\)'
, 'i');
// Valid end-of-path chracters (so /foo. does not gobble the period).
// 1. Allow =&# for empty URL parameters and other URL-join artifacts
regexen.validUrlPathEndingChars = regexSupplant(/[^#{spaces_group}\(\)\?!\*';:=\,\.\$%\[\]#{pd}~&\|@]|(?:#{validUrlBalancedParens})/i);
// Allow @ in a url, but only in the middle. Catch things like http://example.com/@user/
regexen.validUrlPath = regexSupplant('(?:' +
'(?:' +
'#{validGeneralUrlPathChars}*' +
'(?:#{validUrlBalancedParens}#{validGeneralUrlPathChars}*)*' +
'#{validUrlPathEndingChars}'+
')|(?:@#{validGeneralUrlPathChars}+\/)'+
')', 'i');
regexen.validUrlQueryChars = /[a-z0-9!?\*'@\(\);:&=\+\$\/%#\[\]\-_\.,~|]/i;
regexen.validUrlQueryEndingChars = /[a-z0-9_&=#\/]/i;
regexen.validUrl = regexSupplant(
'(' + // $1 URL
'(https?:\\/\\/)' + // $2 Protocol
'(#{validDomain})' + // $3 Domain(s)
'(?::(#{validPortNumber}))?' + // $4 Port number (optional)
'(\\/#{validUrlPath}*)?' + // $5 URL Path
'(\\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?' + // $6 Query String
')'
, 'gi');
return regexen.validUrl;
}());

@ -2,6 +2,7 @@ import React from 'react';
import ComposeFormContainer from '../../compose/containers/compose_form_container'; import ComposeFormContainer from '../../compose/containers/compose_form_container';
import NotificationsContainer from '../../ui/containers/notifications_container'; import NotificationsContainer from '../../ui/containers/notifications_container';
import LoadingBarContainer from '../../ui/containers/loading_bar_container'; import LoadingBarContainer from '../../ui/containers/loading_bar_container';
import ModalContainer from '../../ui/containers/modal_container';
export default class Compose extends React.PureComponent { export default class Compose extends React.PureComponent {
@ -10,6 +11,7 @@ export default class Compose extends React.PureComponent {
<div> <div>
<ComposeFormContainer /> <ComposeFormContainer />
<NotificationsContainer /> <NotificationsContainer />
<ModalContainer />
<LoadingBarContainer className='loading-bar' /> <LoadingBarContainer className='loading-bar' />
</div> </div>
); );

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import punycode from 'punycode'; import punycode from 'punycode';
import classnames from 'classnames'; import classnames from 'classnames';
@ -22,10 +23,15 @@ export default class Card extends React.PureComponent {
static propTypes = { static propTypes = {
card: ImmutablePropTypes.map, card: ImmutablePropTypes.map,
maxDescription: PropTypes.number,
};
static defaultProps = {
maxDescription: 50,
}; };
renderLink () { renderLink () {
const { card } = this.props; const { card, maxDescription } = this.props;
let image = ''; let image = '';
let provider = card.get('provider_name'); let provider = card.get('provider_name');
@ -52,7 +58,7 @@ export default class Card extends React.PureComponent {
<div className='status-card__content'> <div className='status-card__content'>
<strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong> <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>
<p className='status-card__description'>{(card.get('description') || '').substring(0, 50)}</p> <p className='status-card__description'>{(card.get('description') || '').substring(0, maxDescription)}</p>
<span className='status-card__host'>{provider}</span> <span className='status-card__host'>{provider}</span>
</div> </div>
</a> </a>

@ -11,6 +11,7 @@ import Link from 'react-router-dom/Link';
import { FormattedDate, FormattedNumber } from 'react-intl'; import { FormattedDate, FormattedNumber } from 'react-intl';
import CardContainer from '../containers/card_container'; import CardContainer from '../containers/card_container';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import Video from '../../video';
import VisibilityIcon from '../../../../glitch/components/status/visibility_icon'; import VisibilityIcon from '../../../../glitch/components/status/visibility_icon';
export default class DetailedStatus extends ImmutablePureComponent { export default class DetailedStatus extends ImmutablePureComponent {
@ -36,6 +37,10 @@ export default class DetailedStatus extends ImmutablePureComponent {
e.stopPropagation(); e.stopPropagation();
} }
handleOpenVideo = startTime => {
this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), startTime);
}
render () { render () {
const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status; const status = this.props.status.get('reblog') ? this.props.status.get('reblog') : this.props.status;
const { settings } = this.props; const { settings } = this.props;

@ -78,7 +78,7 @@ export default class ColumnsArea extends ImmutablePureComponent {
handleChildrenContentChange() { handleChildrenContentChange() {
if (!this.props.singleColumn) { if (!this.props.singleColumn) {
scrollRight(this.node, this.node.scrollWidth - window.innerWidth); this._interruptScrollAnimation = scrollRight(this.node, this.node.scrollWidth - window.innerWidth);
} }
} }

@ -1,35 +1,29 @@
import React from 'react'; import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ExtendedVideoPlayer from '../../../components/extended_video_player'; import Video from '../../video';
import { defineMessages, injectIntl } from 'react-intl';
import IconButton from '../../../components/icon_button';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
});
@injectIntl
export default class VideoModal extends ImmutablePureComponent { export default class VideoModal extends ImmutablePureComponent {
static propTypes = { static propTypes = {
media: ImmutablePropTypes.map.isRequired, media: ImmutablePropTypes.map.isRequired,
time: PropTypes.number, time: PropTypes.number,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
}; };
render () { render () {
const { media, intl, time, onClose } = this.props; const { media, time, onClose } = this.props;
const url = media.get('url');
return ( return (
<div className='modal-root__modal media-modal'> <div className='modal-root__modal media-modal'>
<div> <div>
<div className='media-modal__close'><IconButton title={intl.formatMessage(messages.close)} icon='times' overlay onClick={onClose} /></div> <Video
<ExtendedVideoPlayer src={url} muted={false} controls time={time} /> preview={media.get('preview_url')}
src={media.get('url')}
startTime={time}
onCloseVideo={onClose}
/>
</div> </div>
</div> </div>
); );

@ -11,7 +11,7 @@ import { debounce } from 'lodash';
import { uploadCompose } from '../../actions/compose'; import { uploadCompose } from '../../actions/compose';
import { refreshHomeTimeline } from '../../actions/timelines'; import { refreshHomeTimeline } from '../../actions/timelines';
import { refreshNotifications } from '../../actions/notifications'; import { refreshNotifications } from '../../actions/notifications';
import { clearStatusesHeight } from '../../actions/statuses'; import { clearHeight } from '../../actions/height_cache';
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers'; import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
import UploadArea from './components/upload_area'; import UploadArea from './components/upload_area';
import ColumnsAreaContainer from './containers/columns_area_container'; import ColumnsAreaContainer from './containers/columns_area_container';
@ -77,7 +77,7 @@ export default class UI extends React.PureComponent {
handleResize = debounce(() => { handleResize = debounce(() => {
// The cached heights are no longer accurate, invalidate // The cached heights are no longer accurate, invalidate
this.props.dispatch(clearStatusesHeight()); this.props.dispatch(clearHeight());
this.setState({ width: window.innerWidth }); this.setState({ width: window.innerWidth });
}, 500, { }, 500, {

@ -109,6 +109,10 @@ export function VideoPlayer () {
return import(/* webpackChunkName: "status/video_player" */'../../../components/video_player'); return import(/* webpackChunkName: "status/video_player" */'../../../components/video_player');
} }
export function Video () {
return import(/* webpackChunkName: "features/video" */'../../video');
}
export function EmbedModal () { export function EmbedModal () {
return import(/* webpackChunkName: "modals/embed_modal" */'../components/embed_modal'); return import(/* webpackChunkName: "modals/embed_modal" */'../components/embed_modal');
} }

@ -0,0 +1,304 @@
import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { throttle } from 'lodash';
import classNames from 'classnames';
const messages = defineMessages({
play: { id: 'video.play', defaultMessage: 'Play' },
pause: { id: 'video.pause', defaultMessage: 'Pause' },
mute: { id: 'video.mute', defaultMessage: 'Mute sound' },
unmute: { id: 'video.unmute', defaultMessage: 'Unmute sound' },
hide: { id: 'video.hide', defaultMessage: 'Hide video' },
expand: { id: 'video.expand', defaultMessage: 'Expand video' },
close: { id: 'video.close', defaultMessage: 'Close video' },
fullscreen: { id: 'video.fullscreen', defaultMessage: 'Full screen' },
exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' },
});
const findElementPosition = el => {
let box;
if (el.getBoundingClientRect && el.parentNode) {
box = el.getBoundingClientRect();
}
if (!box) {
return {
left: 0,
top: 0,
};
}
const docEl = document.documentElement;
const body = document.body;
const clientLeft = docEl.clientLeft || body.clientLeft || 0;
const scrollLeft = window.pageXOffset || body.scrollLeft;
const left = (box.left + scrollLeft) - clientLeft;
const clientTop = docEl.clientTop || body.clientTop || 0;
const scrollTop = window.pageYOffset || body.scrollTop;
const top = (box.top + scrollTop) - clientTop;
return {
left: Math.round(left),
top: Math.round(top),
};
};
const getPointerPosition = (el, event) => {
const position = {};
const box = findElementPosition(el);
const boxW = el.offsetWidth;
const boxH = el.offsetHeight;
const boxY = box.top;
const boxX = box.left;
let pageY = event.pageY;
let pageX = event.pageX;
if (event.changedTouches) {
pageX = event.changedTouches[0].pageX;
pageY = event.changedTouches[0].pageY;
}
position.y = Math.max(0, Math.min(1, ((boxY - pageY) + boxH) / boxH));
position.x = Math.max(0, Math.min(1, (pageX - boxX) / boxW));
return position;
};
const isFullscreen = () => document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement ||
document.msFullscreenElement;
const exitFullscreen = () => {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
};
const requestFullscreen = el => {
if (el.requestFullscreen) {
el.requestFullscreen();
} else if (el.webkitRequestFullscreen) {
el.webkitRequestFullscreen();
} else if (el.mozRequestFullScreen) {
el.mozRequestFullScreen();
} else if (el.msRequestFullscreen) {
el.msRequestFullscreen();
}
};
@injectIntl
export default class Video extends React.PureComponent {
static propTypes = {
preview: PropTypes.string,
src: PropTypes.string.isRequired,
width: PropTypes.number,
height: PropTypes.number,
sensitive: PropTypes.bool,
startTime: PropTypes.number,
onOpenVideo: PropTypes.func,
onCloseVideo: PropTypes.func,
intl: PropTypes.object.isRequired,
};
state = {
progress: 0,
paused: true,
dragging: false,
fullscreen: false,
hovered: false,
muted: false,
revealed: !this.props.sensitive,
};
setPlayerRef = c => {
this.player = c;
}
setVideoRef = c => {
this.video = c;
}
setSeekRef = c => {
this.seek = c;
}
handlePlay = () => {
this.setState({ paused: false });
}
handlePause = () => {
this.setState({ paused: true });
}
handleTimeUpdate = () => {
this.setState({ progress: 100 * (this.video.currentTime / this.video.duration) });
}
handleMouseDown = e => {
document.addEventListener('mousemove', this.handleMouseMove, true);
document.addEventListener('mouseup', this.handleMouseUp, true);
document.addEventListener('touchmove', this.handleMouseMove, true);
document.addEventListener('touchend', this.handleMouseUp, true);
this.setState({ dragging: true });
this.video.pause();
this.handleMouseMove(e);
}
handleMouseUp = () => {
document.removeEventListener('mousemove', this.handleMouseMove, true);
document.removeEventListener('mouseup', this.handleMouseUp, true);
document.removeEventListener('touchmove', this.handleMouseMove, true);
document.removeEventListener('touchend', this.handleMouseUp, true);
this.setState({ dragging: false });
this.video.play();
}
handleMouseMove = throttle(e => {
const { x } = getPointerPosition(this.seek, e);
this.video.currentTime = this.video.duration * x;
this.setState({ progress: x * 100 });
}, 60);
togglePlay = () => {
if (this.state.paused) {
this.video.play();
} else {
this.video.pause();
}
}
toggleFullscreen = () => {
if (isFullscreen()) {
exitFullscreen();
} else {
requestFullscreen(this.player);
}
}
componentDidMount () {
document.addEventListener('fullscreenchange', this.handleFullscreenChange, true);
document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);
document.addEventListener('mozfullscreenchange', this.handleFullscreenChange, true);
document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
}
componentWillUnmount () {
document.removeEventListener('fullscreenchange', this.handleFullscreenChange, true);
document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);
document.removeEventListener('mozfullscreenchange', this.handleFullscreenChange, true);
document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
}
handleFullscreenChange = () => {
this.setState({ fullscreen: isFullscreen() });
}
handleMouseEnter = () => {
this.setState({ hovered: true });
}
handleMouseLeave = () => {
this.setState({ hovered: false });
}
toggleMute = () => {
this.video.muted = !this.video.muted;
this.setState({ muted: this.video.muted });
}
toggleReveal = () => {
if (this.state.revealed) {
this.video.pause();
}
this.setState({ revealed: !this.state.revealed });
}
handleLoadedData = () => {
if (this.props.startTime) {
this.video.currentTime = this.props.startTime;
this.video.play();
}
}
handleOpenVideo = () => {
this.video.pause();
this.props.onOpenVideo(this.video.currentTime);
}
handleCloseVideo = () => {
this.video.pause();
this.props.onCloseVideo();
}
render () {
const { preview, src, width, height, startTime, onOpenVideo, onCloseVideo, intl } = this.props;
const { progress, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
return (
<div className={classNames('video-player', { inactive: !revealed, inline: width && height && !fullscreen, fullscreen })} style={{ width, height }} ref={this.setPlayerRef} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<video
ref={this.setVideoRef}
src={src}
poster={preview}
preload={!!startTime}
loop
role='button'
tabIndex='0'
width={width}
height={height}
onClick={this.togglePlay}
onPlay={this.handlePlay}
onPause={this.handlePause}
onTimeUpdate={this.handleTimeUpdate}
onLoadedData={this.handleLoadedData}
/>
<button className={classNames('video-player__spoiler', { active: !revealed })} onClick={this.toggleReveal}>
<span className='video-player__spoiler__title'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
<span className='video-player__spoiler__subtitle'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
</button>
<div className={classNames('video-player__controls', { active: paused || hovered })}>
<div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}>
<div className='video-player__seek__progress' style={{ width: `${progress}%` }} />
<span
className={classNames('video-player__seek__handle', { active: dragging })}
tabIndex='0'
style={{ left: `${progress}%` }}
/>
</div>
<div className='video-player__buttons left'>
<button aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><i className={classNames('fa fa-fw', { 'fa-play': paused, 'fa-pause': !paused })} /></button>
<button aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><i className={classNames('fa fa-fw', { 'fa-volume-off': muted, 'fa-volume-up': !muted })} /></button>
{!onCloseVideo && <button aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><i className='fa fa-fw fa-eye' /></button>}
</div>
<div className='video-player__buttons right'>
{(!fullscreen && onOpenVideo) && <button aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><i className='fa fa-fw fa-expand' /></button>}
{onCloseVideo && <button aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><i className='fa fa-fw fa-times' /></button>}
<button aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><i className={classNames('fa fa-fw', { 'fa-arrows-alt': !fullscreen, 'fa-compress': fullscreen })} /></button>
</div>
</div>
</div>
);
}
}

@ -33,6 +33,7 @@
"column.home": "الرئيسية", "column.home": "الرئيسية",
"column.mutes": "الحسابات المكتومة", "column.mutes": "الحسابات المكتومة",
"column.notifications": "الإشعارات", "column.notifications": "الإشعارات",
"column.pins": "Pinned toot",
"column.public": "الخيط العام الموحد", "column.public": "الخيط العام الموحد",
"column_back_button.label": "العودة", "column_back_button.label": "العودة",
"column_header.hide_settings": "Hide settings", "column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "معلومات إضافية", "navigation_bar.info": "معلومات إضافية",
"navigation_bar.logout": "خروج", "navigation_bar.logout": "خروج",
"navigation_bar.mutes": "الحسابات المكتومة", "navigation_bar.mutes": "الحسابات المكتومة",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "التفضيلات", "navigation_bar.preferences": "التفضيلات",
"navigation_bar.public_timeline": "الخيط العام الموحد", "navigation_bar.public_timeline": "الخيط العام الموحد",
"notification.favourite": "{name} أعجب بمنشورك", "notification.favourite": "{name} أعجب بمنشورك",
@ -193,6 +195,15 @@
"upload_button.label": "إضافة وسائط", "upload_button.label": "إضافة وسائط",
"upload_form.undo": "إلغاء", "upload_form.undo": "إلغاء",
"upload_progress.label": "يرفع...", "upload_progress.label": "يرفع...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "وسّع الفيديو", "video_player.expand": "وسّع الفيديو",
"video_player.toggle_sound": "تبديل الصوت", "video_player.toggle_sound": "تبديل الصوت",
"video_player.toggle_visible": "إظهار / إخفاء الفيديو", "video_player.toggle_visible": "إظهار / إخفاء الفيديو",

@ -33,6 +33,7 @@
"column.home": "Начало", "column.home": "Начало",
"column.mutes": "Muted users", "column.mutes": "Muted users",
"column.notifications": "Известия", "column.notifications": "Известия",
"column.pins": "Pinned toot",
"column.public": "Публичен канал", "column.public": "Публичен канал",
"column_back_button.label": "Назад", "column_back_button.label": "Назад",
"column_header.hide_settings": "Hide settings", "column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "Extended information", "navigation_bar.info": "Extended information",
"navigation_bar.logout": "Излизане", "navigation_bar.logout": "Излизане",
"navigation_bar.mutes": "Muted users", "navigation_bar.mutes": "Muted users",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Предпочитания", "navigation_bar.preferences": "Предпочитания",
"navigation_bar.public_timeline": "Публичен канал", "navigation_bar.public_timeline": "Публичен канал",
"notification.favourite": "{name} хареса твоята публикация", "notification.favourite": "{name} хареса твоята публикация",
@ -193,6 +195,15 @@
"upload_button.label": "Добави медия", "upload_button.label": "Добави медия",
"upload_form.undo": "Отмяна", "upload_form.undo": "Отмяна",
"upload_progress.label": "Uploading...", "upload_progress.label": "Uploading...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Expand video", "video_player.expand": "Expand video",
"video_player.toggle_sound": "Звук", "video_player.toggle_sound": "Звук",
"video_player.toggle_visible": "Toggle visibility", "video_player.toggle_visible": "Toggle visibility",

@ -33,6 +33,7 @@
"column.home": "Inici", "column.home": "Inici",
"column.mutes": "Usuaris silenciats", "column.mutes": "Usuaris silenciats",
"column.notifications": "Notificacions", "column.notifications": "Notificacions",
"column.pins": "Pinned toot",
"column.public": "Línia de temps federada", "column.public": "Línia de temps federada",
"column_back_button.label": "Enrere", "column_back_button.label": "Enrere",
"column_header.hide_settings": "Hide settings", "column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "Informació addicional", "navigation_bar.info": "Informació addicional",
"navigation_bar.logout": "Tancar sessió", "navigation_bar.logout": "Tancar sessió",
"navigation_bar.mutes": "Usuaris silenciats", "navigation_bar.mutes": "Usuaris silenciats",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Preferències", "navigation_bar.preferences": "Preferències",
"navigation_bar.public_timeline": "Línia de temps federada", "navigation_bar.public_timeline": "Línia de temps federada",
"notification.favourite": "{name} ha afavorit el teu estat", "notification.favourite": "{name} ha afavorit el teu estat",
@ -193,6 +195,15 @@
"upload_button.label": "Afegir multimèdia", "upload_button.label": "Afegir multimèdia",
"upload_form.undo": "Desfer", "upload_form.undo": "Desfer",
"upload_progress.label": "Pujant...", "upload_progress.label": "Pujant...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Ampliar el vídeo", "video_player.expand": "Ampliar el vídeo",
"video_player.toggle_sound": "Alternar so", "video_player.toggle_sound": "Alternar so",
"video_player.toggle_visible": "Alternar visibilitat", "video_player.toggle_visible": "Alternar visibilitat",

@ -33,6 +33,7 @@
"column.home": "Startseite", "column.home": "Startseite",
"column.mutes": "Stummgeschaltete Profile", "column.mutes": "Stummgeschaltete Profile",
"column.notifications": "Mitteilungen", "column.notifications": "Mitteilungen",
"column.pins": "Pinned toot",
"column.public": "Gesamtes bekanntes Netz", "column.public": "Gesamtes bekanntes Netz",
"column_back_button.label": "Zurück", "column_back_button.label": "Zurück",
"column_header.hide_settings": "Einstellungen verbergen", "column_header.hide_settings": "Einstellungen verbergen",
@ -109,6 +110,7 @@
"navigation_bar.info": "Erweiterte Informationen", "navigation_bar.info": "Erweiterte Informationen",
"navigation_bar.logout": "Abmelden", "navigation_bar.logout": "Abmelden",
"navigation_bar.mutes": "Stummgeschaltete Profile", "navigation_bar.mutes": "Stummgeschaltete Profile",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Einstellungen", "navigation_bar.preferences": "Einstellungen",
"navigation_bar.public_timeline": "Föderierte Zeitleiste", "navigation_bar.public_timeline": "Föderierte Zeitleiste",
"notification.favourite": "{name} favorisierte deinen Status", "notification.favourite": "{name} favorisierte deinen Status",
@ -193,6 +195,15 @@
"upload_button.label": "Mediendatei hinzufügen", "upload_button.label": "Mediendatei hinzufügen",
"upload_form.undo": "Entfernen", "upload_form.undo": "Entfernen",
"upload_progress.label": "Lade hoch…", "upload_progress.label": "Lade hoch…",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Videoanzeige vergrößern", "video_player.expand": "Videoanzeige vergrößern",
"video_player.toggle_sound": "Ton umschalten", "video_player.toggle_sound": "Ton umschalten",
"video_player.toggle_visible": "Sichtbarkeit umschalten", "video_player.toggle_visible": "Sichtbarkeit umschalten",

@ -812,6 +812,10 @@
"defaultMessage": "Extended information", "defaultMessage": "Extended information",
"id": "navigation_bar.info" "id": "navigation_bar.info"
}, },
{
"defaultMessage": "Pinned toots",
"id": "navigation_bar.pins"
},
{ {
"defaultMessage": "FAQ", "defaultMessage": "FAQ",
"id": "getting_started.faq" "id": "getting_started.faq"
@ -992,6 +996,15 @@
], ],
"path": "app/javascript/mastodon/features/notifications/index.json" "path": "app/javascript/mastodon/features/notifications/index.json"
}, },
{
"descriptors": [
{
"defaultMessage": "Pinned toot",
"id": "column.pins"
}
],
"path": "app/javascript/mastodon/features/pinned_statuses/index.json"
},
{ {
"descriptors": [ "descriptors": [
{ {
@ -1326,5 +1339,54 @@
} }
], ],
"path": "app/javascript/mastodon/features/ui/components/video_modal.json" "path": "app/javascript/mastodon/features/ui/components/video_modal.json"
},
{
"descriptors": [
{
"defaultMessage": "Play",
"id": "video.play"
},
{
"defaultMessage": "Pause",
"id": "video.pause"
},
{
"defaultMessage": "Mute sound",
"id": "video.mute"
},
{
"defaultMessage": "Unmute sound",
"id": "video.unmute"
},
{
"defaultMessage": "Hide video",
"id": "video.hide"
},
{
"defaultMessage": "Expand video",
"id": "video.expand"
},
{
"defaultMessage": "Close video",
"id": "video.close"
},
{
"defaultMessage": "Full screen",
"id": "video.fullscreen"
},
{
"defaultMessage": "Exit full screen",
"id": "video.exit_fullscreen"
},
{
"defaultMessage": "Sensitive content",
"id": "status.sensitive_warning"
},
{
"defaultMessage": "Click to view",
"id": "status.sensitive_toggle"
}
],
"path": "app/javascript/mastodon/features/video/index.json"
} }
] ]

@ -33,8 +33,8 @@
"column.home": "Home", "column.home": "Home",
"column.mutes": "Muted users", "column.mutes": "Muted users",
"column.notifications": "Notifications", "column.notifications": "Notifications",
"column.public": "Federated timeline",
"column.pins": "Pinned toots", "column.pins": "Pinned toots",
"column.public": "Federated timeline",
"column_back_button.label": "Back", "column_back_button.label": "Back",
"column_header.hide_settings": "Hide settings", "column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left", "column_header.moveLeft_settings": "Move column to the left",
@ -110,9 +110,9 @@
"navigation_bar.info": "About this instance", "navigation_bar.info": "About this instance",
"navigation_bar.logout": "Logout", "navigation_bar.logout": "Logout",
"navigation_bar.mutes": "Muted users", "navigation_bar.mutes": "Muted users",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Preferences", "navigation_bar.preferences": "Preferences",
"navigation_bar.public_timeline": "Federated timeline", "navigation_bar.public_timeline": "Federated timeline",
"navigation_bar.pins": "Pinned toots",
"notification.favourite": "{name} favourited your status", "notification.favourite": "{name} favourited your status",
"notification.follow": "{name} followed you", "notification.follow": "{name} followed you",
"notification.mention": "{name} mentioned you", "notification.mention": "{name} mentioned you",
@ -195,6 +195,15 @@
"upload_button.label": "Add media", "upload_button.label": "Add media",
"upload_form.undo": "Undo", "upload_form.undo": "Undo",
"upload_progress.label": "Uploading...", "upload_progress.label": "Uploading...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Expand video", "video_player.expand": "Expand video",
"video_player.toggle_sound": "Toggle sound", "video_player.toggle_sound": "Toggle sound",
"video_player.toggle_visible": "Toggle visibility", "video_player.toggle_visible": "Toggle visibility",

@ -33,6 +33,7 @@
"column.home": "Hejmo", "column.home": "Hejmo",
"column.mutes": "Muted users", "column.mutes": "Muted users",
"column.notifications": "Sciigoj", "column.notifications": "Sciigoj",
"column.pins": "Pinned toot",
"column.public": "Fratara tempolinio", "column.public": "Fratara tempolinio",
"column_back_button.label": "Reveni", "column_back_button.label": "Reveni",
"column_header.hide_settings": "Hide settings", "column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "Extended information", "navigation_bar.info": "Extended information",
"navigation_bar.logout": "Elsaluti", "navigation_bar.logout": "Elsaluti",
"navigation_bar.mutes": "Muted users", "navigation_bar.mutes": "Muted users",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Preferoj", "navigation_bar.preferences": "Preferoj",
"navigation_bar.public_timeline": "Fratara tempolinio", "navigation_bar.public_timeline": "Fratara tempolinio",
"notification.favourite": "{name} favoris vian mesaĝon", "notification.favourite": "{name} favoris vian mesaĝon",
@ -193,6 +195,15 @@
"upload_button.label": "Aldoni enhavaĵon", "upload_button.label": "Aldoni enhavaĵon",
"upload_form.undo": "Malfari", "upload_form.undo": "Malfari",
"upload_progress.label": "Uploading...", "upload_progress.label": "Uploading...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Expand video", "video_player.expand": "Expand video",
"video_player.toggle_sound": "Aktivigi sonojn", "video_player.toggle_sound": "Aktivigi sonojn",
"video_player.toggle_visible": "Toggle visibility", "video_player.toggle_visible": "Toggle visibility",

@ -1,106 +1,107 @@
{ {
"account.block": "Bloquear", "account.block": "Bloquear",
"account.block_domain": "Hide everything from {domain}", "account.block_domain": "Ocultar todo de {domain}",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.", "account.disclaimer_full": "La siguiente información del usuario puede estar incompleta.",
"account.edit_profile": "Editar perfil", "account.edit_profile": "Editar perfil",
"account.follow": "Seguir", "account.follow": "Seguir",
"account.followers": "Seguidores", "account.followers": "Seguidores",
"account.follows": "Seguir", "account.follows": "Sigue",
"account.follows_you": "Te sigue", "account.follows_you": "Te sigue",
"account.media": "Media", "account.media": "Media",
"account.mention": "Mencionar", "account.mention": "Mencionar a @{name}",
"account.mute": "Silenciar", "account.mute": "Silenciar a @{name}",
"account.posts": "Publicaciones", "account.posts": "Publicaciones",
"account.report": "Report @{name}", "account.report": "Reportar a @{name}",
"account.requested": "Esperando aprobación", "account.requested": "Esperando aprobación",
"account.share": "Share @{name}'s profile", "account.share": "Compartir el perfil de @{name}",
"account.unblock": "Desbloquear", "account.unblock": "Desbloquear a @{name}",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "Mostrar a {domain}",
"account.unfollow": "Dejar de seguir", "account.unfollow": "Dejar de seguir",
"account.unmute": "Unmute @{name}", "account.unmute": "Dejar de silenciar a @{name}",
"account.view_full_profile": "View full profile", "account.view_full_profile": "Ver perfil completo",
"boost_modal.combo": "You can press {combo} to skip this next time", "boost_modal.combo": "Puedes presionar {combo} para saltear este aviso la próxima vez",
"bundle_column_error.body": "Something went wrong while loading this component.", "bundle_column_error.body": "Algo salió mal al cargar este componente.",
"bundle_column_error.retry": "Try again", "bundle_column_error.retry": "Inténtalo de nuevo",
"bundle_column_error.title": "Network error", "bundle_column_error.title": "Error de red",
"bundle_modal_error.close": "Close", "bundle_modal_error.close": "Cerrar",
"bundle_modal_error.message": "Something went wrong while loading this component.", "bundle_modal_error.message": "Algo salió mal al cargar este componente.",
"bundle_modal_error.retry": "Try again", "bundle_modal_error.retry": "Inténtalo de nuevo",
"column.blocks": "Usuarios bloqueados", "column.blocks": "Usuarios bloqueados",
"column.community": "Historia local", "column.community": "Línea de tiempo local",
"column.favourites": "Favoritos", "column.favourites": "Favoritos",
"column.follow_requests": "Solicitudes para seguirte", "column.follow_requests": "Solicitudes de seguimiento",
"column.home": "Inicio", "column.home": "Inicio",
"column.mutes": "Usuarios silenciados", "column.mutes": "Usuarios silenciados",
"column.notifications": "Notificaciones", "column.notifications": "Notificaciones",
"column.pins": "Toot fijado",
"column.public": "Historia federada", "column.public": "Historia federada",
"column_back_button.label": "Atrás", "column_back_button.label": "Atrás",
"column_header.hide_settings": "Hide settings", "column_header.hide_settings": "Ocultar ajustes",
"column_header.moveLeft_settings": "Move column to the left", "column_header.moveLeft_settings": "Mover columna a la izquierda",
"column_header.moveRight_settings": "Move column to the right", "column_header.moveRight_settings": "Mover columna a la derecha",
"column_header.pin": "Pin", "column_header.pin": "Fijar",
"column_header.show_settings": "Show settings", "column_header.show_settings": "Mostrar ajustes",
"column_header.unpin": "Unpin", "column_header.unpin": "Dejar de fijar",
"column_subheading.navigation": "Navigation", "column_subheading.navigation": "Navegación",
"column_subheading.settings": "Settings", "column_subheading.settings": "Ajustes",
"compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", "compose_form.lock_disclaimer": "Tu cuenta no está bloqueada. Todos pueden seguirte para ver tus toots solo para seguidores.",
"compose_form.lock_disclaimer.lock": "locked", "compose_form.lock_disclaimer.lock": "bloqueado",
"compose_form.placeholder": "¿En qué estás pensando?", "compose_form.placeholder": "¿En qué estás pensando?",
"compose_form.privacy_disclaimer": "Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}}? Post privacy only works on Mastodon instances. If {domains} {domainsCount, plural, one {is not a Mastodon instance} other {are not Mastodon instances}}, there will be no indication that your post is private, and it may be boosted or otherwise made visible to unintended recipients.", "compose_form.privacy_disclaimer": "Tu toot privado será enviado a usuario/s mencionados de {domains}. ¿Confías en {domainsCount, plural, one {ese servidor} other {esos servidores}}? La privacidad del toot funcionará solamente en instancias de Mastodon. Si {domains} {domainsCount, plural, one {no es una instancia de Mastodon} other {no son instancias de Mastodon}}, no habrá indicación de que tu toot es privado, y puede hacerse visible a remitentes inesperados.",
"compose_form.publish": "Tootear", "compose_form.publish": "Tootear",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Marcar contenido como sensible", "compose_form.sensitive": "Marcar contenido como sensible",
"compose_form.spoiler": "Ocultar texto tras advertencia", "compose_form.spoiler": "Ocultar texto tras una advertencia",
"compose_form.spoiler_placeholder": "Advertencia de contenido", "compose_form.spoiler_placeholder": "Advertencia de contenido",
"confirmation_modal.cancel": "Cancel", "confirmation_modal.cancel": "Cancelar",
"confirmations.block.confirm": "Block", "confirmations.block.confirm": "Bloquear",
"confirmations.block.message": "Are you sure you want to block {name}?", "confirmations.block.message": "¿Estás seguro de que quieres bloquear a {name}?",
"confirmations.delete.confirm": "Delete", "confirmations.delete.confirm": "Eliminar",
"confirmations.delete.message": "Are you sure you want to delete this status?", "confirmations.delete.message": "¿Estás seguro de que quieres borrar este toot?",
"confirmations.domain_block.confirm": "Hide entire domain", "confirmations.domain_block.confirm": "Ocultar dominio entero",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.", "confirmations.domain_block.message": "¿Seguro de que quieres bloquear al dominio entero? En algunos casos es preferible bloquear o silenciar objetivos determinados.",
"confirmations.mute.confirm": "Mute", "confirmations.mute.confirm": "Silenciar",
"confirmations.mute.message": "Are you sure you want to mute {name}?", "confirmations.mute.message": "¿Estás seguro de que quieres silenciar a {name}?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Dejar de seguir",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "¿Estás seguro de que quieres dejar de seguir a {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "Añade este toot a tu sitio web con el siguiente código.",
"embed.preview": "Here is what it will look like:", "embed.preview": "Así es como se verá:",
"emoji_button.activity": "Activity", "emoji_button.activity": "Actividad",
"emoji_button.flags": "Flags", "emoji_button.flags": "Marcas",
"emoji_button.food": "Food & Drink", "emoji_button.food": "Comida y bebida",
"emoji_button.label": "Insertar emoji", "emoji_button.label": "Insertar emoji",
"emoji_button.nature": "Nature", "emoji_button.nature": "Naturaleza",
"emoji_button.objects": "Objects", "emoji_button.objects": "Objetos",
"emoji_button.people": "People", "emoji_button.people": "Gente",
"emoji_button.search": "Search...", "emoji_button.search": "Buscar…",
"emoji_button.symbols": "Symbols", "emoji_button.symbols": "Símbolos",
"emoji_button.travel": "Travel & Places", "emoji_button.travel": "Viajes y lugares",
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", "empty_column.community": "La línea de tiempo local está vacía. ¡Escribe algo para empezar la fiesta!",
"empty_column.hashtag": "There is nothing in this hashtag yet.", "empty_column.hashtag": "No hay nada en este hashtag aún.",
"empty_column.home": "You aren't following anyone yet. Visit {public} or use search to get started and meet other users.", "empty_column.home": "No estás siguiendo a nadie aún. Visita {public} o haz búsquedas para empezar y conocer gente nueva.",
"empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.", "empty_column.home.inactivity": "Tus notificaciones están vacías. Si has estado inactivo por un tiempo, se regenerará para ti pronto.",
"empty_column.home.public_timeline": "the public timeline", "empty_column.home.public_timeline": "la línea de tiempo pública",
"empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.", "empty_column.notifications": "No tienes ninguna notificación aún. Interactúa con otros para empezar una conversación.",
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other instances to fill it up", "empty_column.public": "¡No hay nada aquí! Escribe algo públicamente, o sigue usuarios de otras instancias manualmente para llenarlo.",
"follow_request.authorize": "Authorize", "follow_request.authorize": "Autorizar",
"follow_request.reject": "Reject", "follow_request.reject": "Rechazar",
"getting_started.appsshort": "Apps", "getting_started.appsshort": "Aplicaciones",
"getting_started.faq": "FAQ", "getting_started.faq": "FAQ",
"getting_started.heading": "Primeros pasos", "getting_started.heading": "Primeros pasos",
"getting_started.open_source_notice": "Mastodon es software libre. Puedes contribuir o reportar errores en {github}.", "getting_started.open_source_notice": "Mastodon es software libre. Puedes contribuir o reportar errores en {github}.",
"getting_started.userguide": "User Guide", "getting_started.userguide": "Guía de usuario",
"home.column_settings.advanced": "Advanced", "home.column_settings.advanced": "Avanzado",
"home.column_settings.basic": "Basic", "home.column_settings.basic": "Básico",
"home.column_settings.filter_regex": "Filter out by regular expressions", "home.column_settings.filter_regex": "Filtrar con expresiones regulares",
"home.column_settings.show_reblogs": "Show boosts", "home.column_settings.show_reblogs": "Mostrar retoots",
"home.column_settings.show_replies": "Show replies", "home.column_settings.show_replies": "Mostrar respuestas",
"home.settings": "Column settings", "home.settings": "Ajustes de columna",
"lightbox.close": "Cerrar", "lightbox.close": "Cerrar",
"lightbox.next": "Next", "lightbox.next": "Siguiente",
"lightbox.previous": "Previous", "lightbox.previous": "Anterior",
"loading_indicator.label": "Cargando...", "loading_indicator.label": "Cargando",
"media_gallery.toggle_visible": "Toggle visibility", "media_gallery.toggle_visible": "Cambiar visibilidad",
"missing_indicator.label": "Not found", "missing_indicator.label": "No encontrado",
"navigation_bar.blocks": "Usuarios bloqueados", "navigation_bar.blocks": "Usuarios bloqueados",
"navigation_bar.community_timeline": "Historia local", "navigation_bar.community_timeline": "Historia local",
"navigation_bar.edit_profile": "Editar perfil", "navigation_bar.edit_profile": "Editar perfil",
@ -109,43 +110,44 @@
"navigation_bar.info": "Información adicional", "navigation_bar.info": "Información adicional",
"navigation_bar.logout": "Cerrar sesión", "navigation_bar.logout": "Cerrar sesión",
"navigation_bar.mutes": "Usuarios silenciados", "navigation_bar.mutes": "Usuarios silenciados",
"navigation_bar.pins": "Toots fijados",
"navigation_bar.preferences": "Preferencias", "navigation_bar.preferences": "Preferencias",
"navigation_bar.public_timeline": "Historia federada", "navigation_bar.public_timeline": "Historia federada",
"notification.favourite": "{name} marcó tu estado como favorito", "notification.favourite": "{name} marcó tu estado como favorito",
"notification.follow": "{name} te empezó a seguir", "notification.follow": "{name} te empezó a seguir",
"notification.mention": "{name} te ha mencionado", "notification.mention": "{name} te ha mencionado",
"notification.reblog": "{name} ha retooteado tu estado", "notification.reblog": "{name} ha retooteado tu estado",
"notifications.clear": "Clear notifications", "notifications.clear": "Limpiar notificaciones",
"notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", "notifications.clear_confirmation": "¿Seguro que quieres limpiar permanentemente todas tus notificaciones?",
"notifications.column_settings.alert": "Notificaciones de escritorio", "notifications.column_settings.alert": "Notificaciones de escritorio",
"notifications.column_settings.favourite": "Favoritos:", "notifications.column_settings.favourite": "Favoritos:",
"notifications.column_settings.follow": "Nuevos seguidores:", "notifications.column_settings.follow": "Nuevos seguidores:",
"notifications.column_settings.mention": "Menciones:", "notifications.column_settings.mention": "Menciones:",
"notifications.column_settings.push": "Push notifications", "notifications.column_settings.push": "Notificaciones push:",
"notifications.column_settings.push_meta": "This device", "notifications.column_settings.push_meta": "Este dispositivo:",
"notifications.column_settings.reblog": "Retoots:", "notifications.column_settings.reblog": "Retoots:",
"notifications.column_settings.show": "Mostrar en columna", "notifications.column_settings.show": "Mostrar en columna",
"notifications.column_settings.sound": "Play sound", "notifications.column_settings.sound": "Reproducir sonido",
"onboarding.done": "Done", "onboarding.done": "Listo",
"onboarding.next": "Next", "onboarding.next": "Siguiente",
"onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", "onboarding.page_five.public_timelines": "La línea de tiempo local muestra toots públicos de todos en {domain}. La línea de tiempo federada muestra toots públicos de cualquiera a quien la gente de {domain} siga. Estas son las líneas de tiempo públicas, una buena forma de conocer gente nueva.",
"onboarding.page_four.home": "The home timeline shows posts from people you follow.", "onboarding.page_four.home": "La línea de tiempo principal muestra toots de gente que sigues.",
"onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.", "onboarding.page_four.notifications": "Las notificaciones se muestran cuando alguien interactúa contigo.",
"onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_one.federation": "Mastodon es una red de servidores federados que conforman una red social aún más grande. Llamamos a estos servidores instancias.",
"onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}", "onboarding.page_one.handle": "Estás en {domain}, así que tu nombre de usuario completo es {handle}",
"onboarding.page_one.welcome": "Welcome to Mastodon!", "onboarding.page_one.welcome": "¡Bienvenido a Mastodon!",
"onboarding.page_six.admin": "Your instance's admin is {admin}.", "onboarding.page_six.admin": "El administrador de tu instancia es {admin}.",
"onboarding.page_six.almost_done": "Almost done...", "onboarding.page_six.almost_done": "Ya casi…",
"onboarding.page_six.appetoot": "Bon Appetoot!", "onboarding.page_six.appetoot": "¡Bon Appetoot!",
"onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.", "onboarding.page_six.apps_available": "Hay {apps} disponibles para iOS, Android y otras plataformas.",
"onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "onboarding.page_six.github": "Mastodon es software libre. Puedes reportar errores, pedir funciones nuevas, o contribuir al código en {github}.",
"onboarding.page_six.guidelines": "community guidelines", "onboarding.page_six.guidelines": "guías de la comunidad",
"onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!", "onboarding.page_six.read_guidelines": "¡Por favor lee las {guidelines} de {domain}!",
"onboarding.page_six.various_app": "mobile apps", "onboarding.page_six.various_app": "aplicaciones móviles",
"onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.", "onboarding.page_three.profile": "Edita tu perfil para cambiar tu avatar, biografía y nombre de cabecera. Ahí, también encontrarás otros ajustes.",
"onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.", "onboarding.page_three.search": "Usa la barra de búsqueda y revisa hashtags, como {illustration} y {introductions}. Para ver a alguien que no es de tu propia instancia, usa su nombre de usuario completo.",
"onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.", "onboarding.page_two.compose": "Escribe toots en la columna de redacción. Puedes subir imágenes, cambiar ajustes de privacidad, y añadir advertencias de contenido con los siguientes íconos.",
"onboarding.skip": "Skip", "onboarding.skip": "Saltar",
"privacy.change": "Ajustar privacidad", "privacy.change": "Ajustar privacidad",
"privacy.direct.long": "Sólo mostrar a los usuarios mencionados", "privacy.direct.long": "Sólo mostrar a los usuarios mencionados",
"privacy.direct.short": "Directo", "privacy.direct.short": "Directo",
@ -156,45 +158,54 @@
"privacy.unlisted.long": "No mostrar en la historia federada", "privacy.unlisted.long": "No mostrar en la historia federada",
"privacy.unlisted.short": "Sin federar", "privacy.unlisted.short": "Sin federar",
"reply_indicator.cancel": "Cancelar", "reply_indicator.cancel": "Cancelar",
"report.placeholder": "Additional comments", "report.placeholder": "Comentarios adicionales",
"report.submit": "Submit", "report.submit": "Publicar",
"report.target": "Reporting", "report.target": "Reportando",
"search.placeholder": "Buscar", "search.placeholder": "Buscar",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}", "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}",
"standalone.public_title": "A look inside...", "standalone.public_title": "Un pequeño vistazo...",
"status.cannot_reblog": "This post cannot be boosted", "status.cannot_reblog": "Este toot no puede retootearse",
"status.delete": "Borrar", "status.delete": "Borrar",
"status.embed": "Embed", "status.embed": "Incrustado",
"status.favourite": "Favorito", "status.favourite": "Favorito",
"status.load_more": "Load more", "status.load_more": "Cargar más",
"status.media_hidden": "Media hidden", "status.media_hidden": "Contenido multimedia oculto",
"status.mention": "Mencionar", "status.mention": "Mencionar",
"status.mute_conversation": "Mute conversation", "status.mute_conversation": "Silenciar conversación",
"status.open": "Expandir estado", "status.open": "Expandir estado",
"status.pin": "Pin on profile", "status.pin": "Fijar",
"status.reblog": "Retoot", "status.reblog": "Retootear",
"status.reblogged_by": "Retooteado por {name}", "status.reblogged_by": "Retooteado por {name}",
"status.reply": "Responder", "status.reply": "Responder",
"status.replyAll": "Reply to thread", "status.replyAll": "Responder al hilo",
"status.report": "Reportar", "status.report": "Reportar",
"status.sensitive_toggle": "Click para ver", "status.sensitive_toggle": "Haz clic para ver",
"status.sensitive_warning": "Contenido sensible", "status.sensitive_warning": "Contenido sensible",
"status.share": "Share", "status.share": "Compartir",
"status.show_less": "Mostrar menos", "status.show_less": "Mostrar menos",
"status.show_more": "Mostrar más", "status.show_more": "Mostrar más",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Dejar de silenciar conversación",
"status.unpin": "Unpin from profile", "status.unpin": "Dejar de fijar",
"tabs_bar.compose": "Redactar", "tabs_bar.compose": "Redactar",
"tabs_bar.federated_timeline": "Federated", "tabs_bar.federated_timeline": "Federado",
"tabs_bar.home": "Inicio", "tabs_bar.home": "Inicio",
"tabs_bar.local_timeline": "Local", "tabs_bar.local_timeline": "Local",
"tabs_bar.notifications": "Notificaciones", "tabs_bar.notifications": "Notificaciones",
"upload_area.title": "Drag & drop to upload", "upload_area.title": "Arrastra y suelta para subir",
"upload_button.label": "Subir multimedia", "upload_button.label": "Subir multimedia",
"upload_form.undo": "Deshacer", "upload_form.undo": "Deshacer",
"upload_progress.label": "Uploading...", "upload_progress.label": "Subiendo…",
"video_player.expand": "Expand video", "video.close": "Cerrar video",
"video_player.toggle_sound": "Act/Desac. sonido", "video.exit_fullscreen": "Salir de pantalla completa",
"video_player.toggle_visible": "Toggle visibility", "video.expand": "Expandir vídeo",
"video_player.video_error": "Video could not be played" "video.fullscreen": "Pantalla completa",
"video.hide": "Ocultar vídeo",
"video.mute": "Silenciar sonido",
"video.pause": "Pausar",
"video.play": "Reproducir",
"video.unmute": "Dejar de silenciar sonido",
"video_player.expand": "Expandir vídeo",
"video_player.toggle_sound": "Activar/Desactivar sonido",
"video_player.toggle_visible": "Cambiar visibilidad",
"video_player.video_error": "No se pudo reproducir el vídeo"
} }

@ -33,6 +33,7 @@
"column.home": "خانه", "column.home": "خانه",
"column.mutes": "کاربران بی‌صداشده", "column.mutes": "کاربران بی‌صداشده",
"column.notifications": "اعلان‌ها", "column.notifications": "اعلان‌ها",
"column.pins": "نوشته‌های ثابت",
"column.public": "نوشته‌های همه‌جا", "column.public": "نوشته‌های همه‌جا",
"column_back_button.label": "بازگشت", "column_back_button.label": "بازگشت",
"column_header.hide_settings": "نهفتن تنظیمات", "column_header.hide_settings": "نهفتن تنظیمات",
@ -109,6 +110,7 @@
"navigation_bar.info": "اطلاعات تکمیلی", "navigation_bar.info": "اطلاعات تکمیلی",
"navigation_bar.logout": "خروج", "navigation_bar.logout": "خروج",
"navigation_bar.mutes": "کاربران بی‌صداشده", "navigation_bar.mutes": "کاربران بی‌صداشده",
"navigation_bar.pins": "نوشته‌های ثابت",
"navigation_bar.preferences": "ترجیحات", "navigation_bar.preferences": "ترجیحات",
"navigation_bar.public_timeline": "نوشته‌های همه‌جا", "navigation_bar.public_timeline": "نوشته‌های همه‌جا",
"notification.favourite": "{name} نوشتهٔ شما را پسندید", "notification.favourite": "{name} نوشتهٔ شما را پسندید",
@ -193,6 +195,15 @@
"upload_button.label": "افزودن تصویر", "upload_button.label": "افزودن تصویر",
"upload_form.undo": "واگردانی", "upload_form.undo": "واگردانی",
"upload_progress.label": "بارگذاری...", "upload_progress.label": "بارگذاری...",
"video.close": "بستن ویدیو",
"video.exit_fullscreen": "خروج از حالت تمام صفحه",
"video.expand": "بزرگ‌کردن ویدیو",
"video.fullscreen": "تمام صفحه",
"video.hide": "نهفتن ویدیو",
"video.mute": "قطع صدا",
"video.pause": "توقف",
"video.play": "پخش",
"video.unmute": "پخش صدا",
"video_player.expand": "بازکردن ویدیو", "video_player.expand": "بازکردن ویدیو",
"video_player.toggle_sound": "تغییر صداداری", "video_player.toggle_sound": "تغییر صداداری",
"video_player.toggle_visible": "تغییر پیدایی", "video_player.toggle_visible": "تغییر پیدایی",

@ -33,6 +33,7 @@
"column.home": "Koti", "column.home": "Koti",
"column.mutes": "Muted users", "column.mutes": "Muted users",
"column.notifications": "Ilmoitukset", "column.notifications": "Ilmoitukset",
"column.pins": "Pinned toot",
"column.public": "Yleinen aikajana", "column.public": "Yleinen aikajana",
"column_back_button.label": "Takaisin", "column_back_button.label": "Takaisin",
"column_header.hide_settings": "Hide settings", "column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "Extended information", "navigation_bar.info": "Extended information",
"navigation_bar.logout": "Kirjaudu ulos", "navigation_bar.logout": "Kirjaudu ulos",
"navigation_bar.mutes": "Muted users", "navigation_bar.mutes": "Muted users",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Ominaisuudet", "navigation_bar.preferences": "Ominaisuudet",
"navigation_bar.public_timeline": "Yleinen aikajana", "navigation_bar.public_timeline": "Yleinen aikajana",
"notification.favourite": "{name} tykkäsi statuksestasi", "notification.favourite": "{name} tykkäsi statuksestasi",
@ -193,6 +195,15 @@
"upload_button.label": "Lisää mediaa", "upload_button.label": "Lisää mediaa",
"upload_form.undo": "Peru", "upload_form.undo": "Peru",
"upload_progress.label": "Uploading...", "upload_progress.label": "Uploading...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Expand video", "video_player.expand": "Expand video",
"video_player.toggle_sound": "Äänet päälle/pois", "video_player.toggle_sound": "Äänet päälle/pois",
"video_player.toggle_visible": "Toggle visibility", "video_player.toggle_visible": "Toggle visibility",

@ -33,8 +33,8 @@
"column.home": "Accueil", "column.home": "Accueil",
"column.mutes": "Comptes masqués", "column.mutes": "Comptes masqués",
"column.notifications": "Notifications", "column.notifications": "Notifications",
"column.public": "Fil public global",
"column.pins": "Pouets épinglés", "column.pins": "Pouets épinglés",
"column.public": "Fil public global",
"column_back_button.label": "Retour", "column_back_button.label": "Retour",
"column_header.hide_settings": "Masquer les paramètres", "column_header.hide_settings": "Masquer les paramètres",
"column_header.moveLeft_settings": "Déplacer la colonne vers la gauche", "column_header.moveLeft_settings": "Déplacer la colonne vers la gauche",
@ -110,9 +110,9 @@
"navigation_bar.info": "Plus dinformations", "navigation_bar.info": "Plus dinformations",
"navigation_bar.logout": "Déconnexion", "navigation_bar.logout": "Déconnexion",
"navigation_bar.mutes": "Comptes masqués", "navigation_bar.mutes": "Comptes masqués",
"navigation_bar.pins": "Pouets épinglés",
"navigation_bar.preferences": "Préférences", "navigation_bar.preferences": "Préférences",
"navigation_bar.public_timeline": "Fil public global", "navigation_bar.public_timeline": "Fil public global",
"navigation_bar.pins": "Pouets épinglés",
"notification.favourite": "{name} a ajouté à ses favoris:", "notification.favourite": "{name} a ajouté à ses favoris:",
"notification.follow": "{name} vous suit.", "notification.follow": "{name} vous suit.",
"notification.mention": "{name} vous a mentionné⋅e:", "notification.mention": "{name} vous a mentionné⋅e:",
@ -166,7 +166,7 @@
"standalone.public_title": "Jeter un coup dœil…", "standalone.public_title": "Jeter un coup dœil…",
"status.cannot_reblog": "Cette publication ne peut être boostée", "status.cannot_reblog": "Cette publication ne peut être boostée",
"status.delete": "Effacer", "status.delete": "Effacer",
"status.embed": "Embed", "status.embed": "Intégrer",
"status.favourite": "Ajouter aux favoris", "status.favourite": "Ajouter aux favoris",
"status.load_more": "Charger plus", "status.load_more": "Charger plus",
"status.media_hidden": "Média caché", "status.media_hidden": "Média caché",
@ -195,6 +195,15 @@
"upload_button.label": "Joindre un média", "upload_button.label": "Joindre un média",
"upload_form.undo": "Annuler", "upload_form.undo": "Annuler",
"upload_progress.label": "Envoi en cours…", "upload_progress.label": "Envoi en cours…",
"video.close": "Fermer la vidéo",
"video.exit_fullscreen": "Quitter plein écran",
"video.expand": "Agrandir la vidéo",
"video.fullscreen": "Plein écran",
"video.hide": "Masquer la vidéo",
"video.mute": "Couper le son",
"video.pause": "Pause",
"video.play": "Lecture",
"video.unmute": "Rétablir le son",
"video_player.expand": "Agrandir la vidéo", "video_player.expand": "Agrandir la vidéo",
"video_player.toggle_sound": "Activer/Désactiver le son", "video_player.toggle_sound": "Activer/Désactiver le son",
"video_player.toggle_visible": "Afficher/Cacher la vidéo", "video_player.toggle_visible": "Afficher/Cacher la vidéo",

@ -33,6 +33,7 @@
"column.home": "בבית", "column.home": "בבית",
"column.mutes": "השתקות", "column.mutes": "השתקות",
"column.notifications": "התראות", "column.notifications": "התראות",
"column.pins": "Pinned toot",
"column.public": "בפרהסיה", "column.public": "בפרהסיה",
"column_back_button.label": "חזרה", "column_back_button.label": "חזרה",
"column_header.hide_settings": "Hide settings", "column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "מידע נוסף", "navigation_bar.info": "מידע נוסף",
"navigation_bar.logout": "יציאה", "navigation_bar.logout": "יציאה",
"navigation_bar.mutes": "השתקות", "navigation_bar.mutes": "השתקות",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "העדפות", "navigation_bar.preferences": "העדפות",
"navigation_bar.public_timeline": "ציר זמן בין-קהילתי", "navigation_bar.public_timeline": "ציר זמן בין-קהילתי",
"notification.favourite": "חצרוצך חובב על ידי {name}", "notification.favourite": "חצרוצך חובב על ידי {name}",
@ -193,6 +195,15 @@
"upload_button.label": "הוספת מדיה", "upload_button.label": "הוספת מדיה",
"upload_form.undo": "ביטול", "upload_form.undo": "ביטול",
"upload_progress.label": "עולה...", "upload_progress.label": "עולה...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "הרחבת וידאו", "video_player.expand": "הרחבת וידאו",
"video_player.toggle_sound": "הפעלת\\ביטול שמע", "video_player.toggle_sound": "הפעלת\\ביטול שמע",
"video_player.toggle_visible": "הפעלת\\ביטול תצוגה", "video_player.toggle_visible": "הפעלת\\ביטול תצוגה",

@ -33,6 +33,7 @@
"column.home": "Dom", "column.home": "Dom",
"column.mutes": "Utišani korisnici", "column.mutes": "Utišani korisnici",
"column.notifications": "Notifikacije", "column.notifications": "Notifikacije",
"column.pins": "Pinned toot",
"column.public": "Federalni timeline", "column.public": "Federalni timeline",
"column_back_button.label": "Natrag", "column_back_button.label": "Natrag",
"column_header.hide_settings": "Hide settings", "column_header.hide_settings": "Hide settings",
@ -61,7 +62,6 @@
"confirmations.domain_block.message": "Jesi li zaista, zaista siguran da želiš potpuno blokirati {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.", "confirmations.domain_block.message": "Jesi li zaista, zaista siguran da želiš potpuno blokirati {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.",
"confirmations.mute.confirm": "Utišaj", "confirmations.mute.confirm": "Utišaj",
"confirmations.mute.message": "Jesi li siguran da želiš utišati {name}?", "confirmations.mute.message": "Jesi li siguran da želiš utišati {name}?",
"confirmations.mute.message": "Jesi li siguran da želiš utišati {name}?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "Embed this status on your website by copying the code below.",
@ -110,6 +110,7 @@
"navigation_bar.info": "Više informacija", "navigation_bar.info": "Više informacija",
"navigation_bar.logout": "Odjavi se", "navigation_bar.logout": "Odjavi se",
"navigation_bar.mutes": "Utišani korisnici", "navigation_bar.mutes": "Utišani korisnici",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Postavke", "navigation_bar.preferences": "Postavke",
"navigation_bar.public_timeline": "Federalni timeline", "navigation_bar.public_timeline": "Federalni timeline",
"notification.favourite": "{name} je lajkao tvoj status", "notification.favourite": "{name} je lajkao tvoj status",
@ -194,6 +195,15 @@
"upload_button.label": "Dodaj media", "upload_button.label": "Dodaj media",
"upload_form.undo": "Poništi", "upload_form.undo": "Poništi",
"upload_progress.label": "Uploadam...", "upload_progress.label": "Uploadam...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Proširi video", "video_player.expand": "Proširi video",
"video_player.toggle_sound": "Toggle zvuk", "video_player.toggle_sound": "Toggle zvuk",
"video_player.toggle_visible": "Preklopi vidljivost", "video_player.toggle_visible": "Preklopi vidljivost",

@ -33,6 +33,7 @@
"column.home": "Kezdőlap", "column.home": "Kezdőlap",
"column.mutes": "Muted users", "column.mutes": "Muted users",
"column.notifications": "Értesítések", "column.notifications": "Értesítések",
"column.pins": "Pinned toot",
"column.public": "Nyilvános", "column.public": "Nyilvános",
"column_back_button.label": "Vissza", "column_back_button.label": "Vissza",
"column_header.hide_settings": "Hide settings", "column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "Extended information", "navigation_bar.info": "Extended information",
"navigation_bar.logout": "Kijelentkezés", "navigation_bar.logout": "Kijelentkezés",
"navigation_bar.mutes": "Muted users", "navigation_bar.mutes": "Muted users",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Beállítások", "navigation_bar.preferences": "Beállítások",
"navigation_bar.public_timeline": "Nyilvános időfolyam", "navigation_bar.public_timeline": "Nyilvános időfolyam",
"notification.favourite": "{name} kedvencnek jelölte az állapotod", "notification.favourite": "{name} kedvencnek jelölte az állapotod",
@ -193,6 +195,15 @@
"upload_button.label": "Média hozzáadása", "upload_button.label": "Média hozzáadása",
"upload_form.undo": "Mégsem", "upload_form.undo": "Mégsem",
"upload_progress.label": "Uploading...", "upload_progress.label": "Uploading...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Expand video", "video_player.expand": "Expand video",
"video_player.toggle_sound": "Hang kapcsolása", "video_player.toggle_sound": "Hang kapcsolása",
"video_player.toggle_visible": "Toggle visibility", "video_player.toggle_visible": "Toggle visibility",

@ -33,6 +33,7 @@
"column.home": "Beranda", "column.home": "Beranda",
"column.mutes": "Pengguna dibisukan", "column.mutes": "Pengguna dibisukan",
"column.notifications": "Notifikasi", "column.notifications": "Notifikasi",
"column.pins": "Pinned toot",
"column.public": "Linimasa gabunggan", "column.public": "Linimasa gabunggan",
"column_back_button.label": "Kembali", "column_back_button.label": "Kembali",
"column_header.hide_settings": "Hide settings", "column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "Informasi selengkapnya", "navigation_bar.info": "Informasi selengkapnya",
"navigation_bar.logout": "Keluar", "navigation_bar.logout": "Keluar",
"navigation_bar.mutes": "Pengguna dibisukan", "navigation_bar.mutes": "Pengguna dibisukan",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Pengaturan", "navigation_bar.preferences": "Pengaturan",
"navigation_bar.public_timeline": "Linimasa gabungan", "navigation_bar.public_timeline": "Linimasa gabungan",
"notification.favourite": "{name} menyukai status anda", "notification.favourite": "{name} menyukai status anda",
@ -193,6 +195,15 @@
"upload_button.label": "Tambahkan media", "upload_button.label": "Tambahkan media",
"upload_form.undo": "Undo", "upload_form.undo": "Undo",
"upload_progress.label": "Mengunggah...", "upload_progress.label": "Mengunggah...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Tampilkan video", "video_player.expand": "Tampilkan video",
"video_player.toggle_sound": "Suara", "video_player.toggle_sound": "Suara",
"video_player.toggle_visible": "Tampilan", "video_player.toggle_visible": "Tampilan",

@ -33,6 +33,7 @@
"column.home": "Hemo", "column.home": "Hemo",
"column.mutes": "Celita uzeri", "column.mutes": "Celita uzeri",
"column.notifications": "Savigi", "column.notifications": "Savigi",
"column.pins": "Pinned toot",
"column.public": "Federata tempolineo", "column.public": "Federata tempolineo",
"column_back_button.label": "Retro", "column_back_button.label": "Retro",
"column_header.hide_settings": "Hide settings", "column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "Detaloza informi", "navigation_bar.info": "Detaloza informi",
"navigation_bar.logout": "Ekirar", "navigation_bar.logout": "Ekirar",
"navigation_bar.mutes": "Celita uzeri", "navigation_bar.mutes": "Celita uzeri",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Preferi", "navigation_bar.preferences": "Preferi",
"navigation_bar.public_timeline": "Federata tempolineo", "navigation_bar.public_timeline": "Federata tempolineo",
"notification.favourite": "{name} favorizis tua mesajo", "notification.favourite": "{name} favorizis tua mesajo",
@ -193,6 +195,15 @@
"upload_button.label": "Adjuntar kontenajo", "upload_button.label": "Adjuntar kontenajo",
"upload_form.undo": "Desfacar", "upload_form.undo": "Desfacar",
"upload_progress.label": "Kargante...", "upload_progress.label": "Kargante...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Extensar video", "video_player.expand": "Extensar video",
"video_player.toggle_sound": "Acendar sono", "video_player.toggle_sound": "Acendar sono",
"video_player.toggle_visible": "Chanjar videbleso", "video_player.toggle_visible": "Chanjar videbleso",

@ -33,6 +33,7 @@
"column.home": "Home", "column.home": "Home",
"column.mutes": "Utenti silenziati", "column.mutes": "Utenti silenziati",
"column.notifications": "Notifiche", "column.notifications": "Notifiche",
"column.pins": "Pinned toot",
"column.public": "Timeline federata", "column.public": "Timeline federata",
"column_back_button.label": "Indietro", "column_back_button.label": "Indietro",
"column_header.hide_settings": "Hide settings", "column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "Informazioni estese", "navigation_bar.info": "Informazioni estese",
"navigation_bar.logout": "Logout", "navigation_bar.logout": "Logout",
"navigation_bar.mutes": "Utenti silenziati", "navigation_bar.mutes": "Utenti silenziati",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Impostazioni", "navigation_bar.preferences": "Impostazioni",
"navigation_bar.public_timeline": "Timeline federata", "navigation_bar.public_timeline": "Timeline federata",
"notification.favourite": "{name} ha apprezzato il tuo post", "notification.favourite": "{name} ha apprezzato il tuo post",
@ -193,6 +195,15 @@
"upload_button.label": "Aggiungi file multimediale", "upload_button.label": "Aggiungi file multimediale",
"upload_form.undo": "Annulla", "upload_form.undo": "Annulla",
"upload_progress.label": "Sto caricando...", "upload_progress.label": "Sto caricando...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Espandi video", "video_player.expand": "Espandi video",
"video_player.toggle_sound": "Attiva suono", "video_player.toggle_sound": "Attiva suono",
"video_player.toggle_visible": "Attiva visibilità", "video_player.toggle_visible": "Attiva visibilità",

@ -33,8 +33,8 @@
"column.home": "ホーム", "column.home": "ホーム",
"column.mutes": "ミュートしたユーザー", "column.mutes": "ミュートしたユーザー",
"column.notifications": "通知", "column.notifications": "通知",
"column.public": "連合タイムライン",
"column.pins": "固定されたトゥート", "column.pins": "固定されたトゥート",
"column.public": "連合タイムライン",
"column_back_button.label": "戻る", "column_back_button.label": "戻る",
"column_header.hide_settings": "設定を隠す", "column_header.hide_settings": "設定を隠す",
"column_header.moveLeft_settings": "カラムを左に移動する", "column_header.moveLeft_settings": "カラムを左に移動する",
@ -97,8 +97,8 @@
"home.column_settings.show_replies": "返信表示", "home.column_settings.show_replies": "返信表示",
"home.settings": "カラム設定", "home.settings": "カラム設定",
"lightbox.close": "閉じる", "lightbox.close": "閉じる",
"lightbox.next": "Next", "lightbox.next": "",
"lightbox.previous": "Previous", "lightbox.previous": "",
"loading_indicator.label": "読み込み中...", "loading_indicator.label": "読み込み中...",
"media_gallery.toggle_visible": "表示切り替え", "media_gallery.toggle_visible": "表示切り替え",
"missing_indicator.label": "見つかりません", "missing_indicator.label": "見つかりません",
@ -110,9 +110,9 @@
"navigation_bar.info": "このインスタンスについて", "navigation_bar.info": "このインスタンスについて",
"navigation_bar.logout": "ログアウト", "navigation_bar.logout": "ログアウト",
"navigation_bar.mutes": "ミュートしたユーザー", "navigation_bar.mutes": "ミュートしたユーザー",
"navigation_bar.pins": "固定されたトゥート",
"navigation_bar.preferences": "ユーザー設定", "navigation_bar.preferences": "ユーザー設定",
"navigation_bar.public_timeline": "連合タイムライン", "navigation_bar.public_timeline": "連合タイムライン",
"navigation_bar.pins": "固定されたトゥート",
"notification.favourite": "{name}さんがあなたのトゥートをお気に入りに登録しました", "notification.favourite": "{name}さんがあなたのトゥートをお気に入りに登録しました",
"notification.follow": "{name}さんにフォローされました", "notification.follow": "{name}さんにフォローされました",
"notification.mention": "{name}さんがあなたに返信しました", "notification.mention": "{name}さんがあなたに返信しました",
@ -195,6 +195,15 @@
"upload_button.label": "メディアを追加", "upload_button.label": "メディアを追加",
"upload_form.undo": "やり直す", "upload_form.undo": "やり直す",
"upload_progress.label": "アップロード中...", "upload_progress.label": "アップロード中...",
"video.close": "動画を閉じる",
"video.exit_fullscreen": "全画面を終了する",
"video.expand": "動画を拡大する",
"video.fullscreen": "全画面",
"video.hide": "動画を閉じる",
"video.mute": "ミュート",
"video.pause": "一時停止",
"video.play": "再生",
"video.unmute": "ミュートを解除する",
"video_player.expand": "動画の詳細", "video_player.expand": "動画の詳細",
"video_player.toggle_sound": "音の切り替え", "video_player.toggle_sound": "音の切り替え",
"video_player.toggle_visible": "表示切り替え", "video_player.toggle_visible": "表示切り替え",

@ -33,8 +33,8 @@
"column.home": "홈", "column.home": "홈",
"column.mutes": "뮤트 중인 사용자", "column.mutes": "뮤트 중인 사용자",
"column.notifications": "알림", "column.notifications": "알림",
"column.public": "연합 타임라인",
"column.pins": "고정된 Toot", "column.pins": "고정된 Toot",
"column.public": "연합 타임라인",
"column_back_button.label": "돌아가기", "column_back_button.label": "돌아가기",
"column_header.hide_settings": "Hide settings", "column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left", "column_header.moveLeft_settings": "Move column to the left",
@ -110,9 +110,9 @@
"navigation_bar.info": "이 인스턴스에 대해서", "navigation_bar.info": "이 인스턴스에 대해서",
"navigation_bar.logout": "로그아웃", "navigation_bar.logout": "로그아웃",
"navigation_bar.mutes": "뮤트 중인 사용자", "navigation_bar.mutes": "뮤트 중인 사용자",
"navigation_bar.pins": "고정된 Toot",
"navigation_bar.preferences": "사용자 설정", "navigation_bar.preferences": "사용자 설정",
"navigation_bar.public_timeline": "연합 타임라인", "navigation_bar.public_timeline": "연합 타임라인",
"navigation_bar.pins": "고정된 Toot",
"notification.favourite": "{name}님이 즐겨찾기 했습니다", "notification.favourite": "{name}님이 즐겨찾기 했습니다",
"notification.follow": "{name}님이 나를 팔로우 했습니다", "notification.follow": "{name}님이 나를 팔로우 했습니다",
"notification.mention": "{name}님이 답글을 보냈습니다", "notification.mention": "{name}님이 답글을 보냈습니다",
@ -195,6 +195,15 @@
"upload_button.label": "미디어 추가", "upload_button.label": "미디어 추가",
"upload_form.undo": "재시도", "upload_form.undo": "재시도",
"upload_progress.label": "업로드 중...", "upload_progress.label": "업로드 중...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "동영상 자세히 보기", "video_player.expand": "동영상 자세히 보기",
"video_player.toggle_sound": "소리 토글하기", "video_player.toggle_sound": "소리 토글하기",
"video_player.toggle_visible": "표시 전환", "video_player.toggle_visible": "표시 전환",

@ -12,7 +12,7 @@
"account.mute": "Negeer @{name}", "account.mute": "Negeer @{name}",
"account.posts": "Toots", "account.posts": "Toots",
"account.report": "Rapporteer @{name}", "account.report": "Rapporteer @{name}",
"account.requested": "Wacht op goedkeuring", "account.requested": "Wacht op goedkeuring. Klik om volgverzoek te annuleren.",
"account.share": "Profiel van @{name} delen", "account.share": "Profiel van @{name} delen",
"account.unblock": "Deblokkeer @{name}", "account.unblock": "Deblokkeer @{name}",
"account.unblock_domain": "{domain} niet meer negeren", "account.unblock_domain": "{domain} niet meer negeren",
@ -33,11 +33,13 @@
"column.home": "Start", "column.home": "Start",
"column.mutes": "Genegeerde gebruikers", "column.mutes": "Genegeerde gebruikers",
"column.notifications": "Meldingen", "column.notifications": "Meldingen",
"column.pins": "Pinned toot",
"column.public": "Globale tijdlijn", "column.public": "Globale tijdlijn",
"column.pins": "Vastgezette toots",
"column_back_button.label": "terug", "column_back_button.label": "terug",
"column_header.hide_settings": "Instellingen verbergen", "column_header.hide_settings": "Instellingen verbergen",
"column_header.moveLeft_settings": "Move column to the left", "column_header.moveLeft_settings": "Kolom naar links verplaatsen",
"column_header.moveRight_settings": "Move column to the right", "column_header.moveRight_settings": "Kolom naar rechts verplaatsen",
"column_header.pin": "Vastmaken", "column_header.pin": "Vastmaken",
"column_header.show_settings": "Instellingen tonen", "column_header.show_settings": "Instellingen tonen",
"column_header.unpin": "Losmaken", "column_header.unpin": "Losmaken",
@ -63,8 +65,8 @@
"confirmations.mute.message": "Weet je het zeker dat je {name} wilt negeren?", "confirmations.mute.message": "Weet je het zeker dat je {name} wilt negeren?",
"confirmations.unfollow.confirm": "Ontvolgen", "confirmations.unfollow.confirm": "Ontvolgen",
"confirmations.unfollow.message": "Weet je het zeker dat je {name} wilt ontvolgen?", "confirmations.unfollow.message": "Weet je het zeker dat je {name} wilt ontvolgen?",
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "Embed deze toot op jouw website, door de onderstaande code te kopiëren.",
"embed.preview": "Here is what it will look like:", "embed.preview": "Zo komt het eruit te zien:",
"emoji_button.activity": "Activiteiten", "emoji_button.activity": "Activiteiten",
"emoji_button.flags": "Vlaggen", "emoji_button.flags": "Vlaggen",
"emoji_button.food": "Eten en drinken", "emoji_button.food": "Eten en drinken",
@ -85,6 +87,7 @@
"follow_request.authorize": "Goedkeuren", "follow_request.authorize": "Goedkeuren",
"follow_request.reject": "Afkeuren", "follow_request.reject": "Afkeuren",
"getting_started.appsshort": "Apps", "getting_started.appsshort": "Apps",
"getting_started.donate": "Doneren",
"getting_started.faq": "FAQ", "getting_started.faq": "FAQ",
"getting_started.heading": "Beginnen", "getting_started.heading": "Beginnen",
"getting_started.open_source_notice": "Mastodon is open-sourcesoftware. Je kunt bijdragen of problemen melden op GitHub via {github}.", "getting_started.open_source_notice": "Mastodon is open-sourcesoftware. Je kunt bijdragen of problemen melden op GitHub via {github}.",
@ -109,8 +112,10 @@
"navigation_bar.info": "Uitgebreide informatie", "navigation_bar.info": "Uitgebreide informatie",
"navigation_bar.logout": "Afmelden", "navigation_bar.logout": "Afmelden",
"navigation_bar.mutes": "Genegeerde gebruikers", "navigation_bar.mutes": "Genegeerde gebruikers",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Instellingen", "navigation_bar.preferences": "Instellingen",
"navigation_bar.public_timeline": "Globale tijdlijn", "navigation_bar.public_timeline": "Globale tijdlijn",
"navigation_bar.pins": "Vastgezette toots",
"notification.favourite": "{name} markeerde jouw toot als favoriet", "notification.favourite": "{name} markeerde jouw toot als favoriet",
"notification.follow": "{name} volgt jou nu", "notification.follow": "{name} volgt jou nu",
"notification.mention": "{name} vermeldde jou", "notification.mention": "{name} vermeldde jou",
@ -171,7 +176,7 @@
"status.mention": "Vermeld @{name}", "status.mention": "Vermeld @{name}",
"status.mute_conversation": "Negeer conversatie", "status.mute_conversation": "Negeer conversatie",
"status.open": "Toot volledig tonen", "status.open": "Toot volledig tonen",
"status.pin": "Pin on profile", "status.pin": "Aan profielpagina vastmaken",
"status.reblog": "Boost", "status.reblog": "Boost",
"status.reblogged_by": "{name} boostte", "status.reblogged_by": "{name} boostte",
"status.reply": "Reageren", "status.reply": "Reageren",
@ -183,7 +188,7 @@
"status.show_less": "Minder tonen", "status.show_less": "Minder tonen",
"status.show_more": "Meer tonen", "status.show_more": "Meer tonen",
"status.unmute_conversation": "Conversatie niet meer negeren", "status.unmute_conversation": "Conversatie niet meer negeren",
"status.unpin": "Unpin from profile", "status.unpin": "Van profielpagina losmaken",
"tabs_bar.compose": "Schrijven", "tabs_bar.compose": "Schrijven",
"tabs_bar.federated_timeline": "Globaal", "tabs_bar.federated_timeline": "Globaal",
"tabs_bar.home": "Start", "tabs_bar.home": "Start",
@ -193,6 +198,15 @@
"upload_button.label": "Media toevoegen", "upload_button.label": "Media toevoegen",
"upload_form.undo": "Ongedaan maken", "upload_form.undo": "Ongedaan maken",
"upload_progress.label": "Uploaden...", "upload_progress.label": "Uploaden...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Video groter maken",
"video.fullscreen": "Volledig scherm",
"video.hide": "Video verbergen",
"video.mute": "Geluid uitschakelen",
"video.pause": "Pauze",
"video.play": "Afspelen",
"video.unmute": "Geluid inschakelen",
"video_player.expand": "Video groter maken", "video_player.expand": "Video groter maken",
"video_player.toggle_sound": "Geluid in-/uitschakelen", "video_player.toggle_sound": "Geluid in-/uitschakelen",
"video_player.toggle_visible": "Video wel/niet tonen", "video_player.toggle_visible": "Video wel/niet tonen",

@ -33,6 +33,7 @@
"column.home": "Hjem", "column.home": "Hjem",
"column.mutes": "Dempede brukere", "column.mutes": "Dempede brukere",
"column.notifications": "Varsler", "column.notifications": "Varsler",
"column.pins": "Pinned toot",
"column.public": "Felles tidslinje", "column.public": "Felles tidslinje",
"column_back_button.label": "Tilbake", "column_back_button.label": "Tilbake",
"column_header.hide_settings": "Hide settings", "column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "Utvidet informasjon", "navigation_bar.info": "Utvidet informasjon",
"navigation_bar.logout": "Logg ut", "navigation_bar.logout": "Logg ut",
"navigation_bar.mutes": "Dempede brukere", "navigation_bar.mutes": "Dempede brukere",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Preferanser", "navigation_bar.preferences": "Preferanser",
"navigation_bar.public_timeline": "Felles tidslinje", "navigation_bar.public_timeline": "Felles tidslinje",
"notification.favourite": "{name} likte din status", "notification.favourite": "{name} likte din status",
@ -193,6 +195,15 @@
"upload_button.label": "Legg til media", "upload_button.label": "Legg til media",
"upload_form.undo": "Angre", "upload_form.undo": "Angre",
"upload_progress.label": "Laster opp...", "upload_progress.label": "Laster opp...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Utvid video", "video_player.expand": "Utvid video",
"video_player.toggle_sound": "Veksle lyd", "video_player.toggle_sound": "Veksle lyd",
"video_player.toggle_visible": "Veksle synlighet", "video_player.toggle_visible": "Veksle synlighet",

@ -33,8 +33,8 @@
"column.home": "Acuèlh", "column.home": "Acuèlh",
"column.mutes": "Personas en silenci", "column.mutes": "Personas en silenci",
"column.notifications": "Notificacions", "column.notifications": "Notificacions",
"column.public": "Flux public global",
"column.pins": "Tuts penjats", "column.pins": "Tuts penjats",
"column.public": "Flux public global",
"column_back_button.label": "Tornar", "column_back_button.label": "Tornar",
"column_header.hide_settings": "Amagar los paramètres", "column_header.hide_settings": "Amagar los paramètres",
"column_header.moveLeft_settings": "Desplaçar la colomna a man drecha", "column_header.moveLeft_settings": "Desplaçar la colomna a man drecha",
@ -64,7 +64,7 @@
"confirmations.mute.message": "Sètz segur de voler metre en silenci {name}?", "confirmations.mute.message": "Sètz segur de voler metre en silenci {name}?",
"confirmations.unfollow.confirm": "Quitar de sègre", "confirmations.unfollow.confirm": "Quitar de sègre",
"confirmations.unfollow.message": "Volètz vertadièrament quitar de sègre {name}?", "confirmations.unfollow.message": "Volètz vertadièrament quitar de sègre {name}?",
"embed.instructions": "Embarcar aqueste estatut per o far veire sus un site Internet en copiar lo còdi çai-jos.", "embed.instructions": "Embarcar aqueste estatut per lo far veire sus un site Internet en copiar lo còdi çai-jos.",
"embed.preview": "Semblarà aquò:", "embed.preview": "Semblarà aquò:",
"emoji_button.activity": "Activitats", "emoji_button.activity": "Activitats",
"emoji_button.flags": "Drapèus", "emoji_button.flags": "Drapèus",
@ -110,9 +110,9 @@
"navigation_bar.info": "Mai informacions", "navigation_bar.info": "Mai informacions",
"navigation_bar.logout": "Desconnexion", "navigation_bar.logout": "Desconnexion",
"navigation_bar.mutes": "Personas rescondudas", "navigation_bar.mutes": "Personas rescondudas",
"navigation_bar.pins": "Tuts penjats",
"navigation_bar.preferences": "Preferéncias", "navigation_bar.preferences": "Preferéncias",
"navigation_bar.public_timeline": "Flux public global", "navigation_bar.public_timeline": "Flux public global",
"navigation_bar.pins": "Tuts penjats",
"notification.favourite": "{name} a ajustat a sos favorits:", "notification.favourite": "{name} a ajustat a sos favorits:",
"notification.follow": "{name} vos sèc", "notification.follow": "{name} vos sèc",
"notification.mention": "{name} vos a mencionat:", "notification.mention": "{name} vos a mencionat:",
@ -195,6 +195,15 @@
"upload_button.label": "Ajustar un mèdia", "upload_button.label": "Ajustar un mèdia",
"upload_form.undo": "Anullar", "upload_form.undo": "Anullar",
"upload_progress.label": "Mandadís…", "upload_progress.label": "Mandadís…",
"video.close": "Tampar la vidèo",
"video.exit_fullscreen": "Sortir plen ecran",
"video.expand": "Agrandir la vidèo",
"video.fullscreen": "Ecran complet",
"video.hide": "Amagar la vidèo",
"video.mute": "Copar lo son",
"video.pause": "Pausa",
"video.play": "Lectura",
"video.unmute": "Restablir lo son",
"video_player.expand": "Mostrar la vidèo", "video_player.expand": "Mostrar la vidèo",
"video_player.toggle_sound": "Activar/Desactivar lo son", "video_player.toggle_sound": "Activar/Desactivar lo son",
"video_player.toggle_visible": "Mostrar/Rescondre la vidèo", "video_player.toggle_visible": "Mostrar/Rescondre la vidèo",

@ -195,7 +195,16 @@
"upload_button.label": "Dodaj zawartość multimedialną", "upload_button.label": "Dodaj zawartość multimedialną",
"upload_form.undo": "Cofnij", "upload_form.undo": "Cofnij",
"upload_progress.label": "Wysyłanie", "upload_progress.label": "Wysyłanie",
"video_player.expand": "Przełącz wideo", "video.close": "Zamknij film",
"video.exit_fullscreen": "Opuść tryb pełnoekranowy",
"video.expand": "Rozszerz film",
"video.fullscreen": "Pełny ekran",
"video.hide": "Ukryj film",
"video.mute": "Wycisz",
"video.pause": "Pauzuj",
"video.play": "Odtwórz",
"video.unmute": "Cofnij wyciszenie",
"video_player.expand": "Rozszerz film",
"video_player.toggle_sound": "Przełącz dźwięk", "video_player.toggle_sound": "Przełącz dźwięk",
"video_player.toggle_visible": "Przełącz widoczność", "video_player.toggle_visible": "Przełącz widoczność",
"video_player.video_error": "Nie można odtworzyć pliku wideo" "video_player.video_error": "Nie można odtworzyć pliku wideo"

@ -6,25 +6,25 @@
"account.follow": "Seguir", "account.follow": "Seguir",
"account.followers": "Seguidores", "account.followers": "Seguidores",
"account.follows": "Segue", "account.follows": "Segue",
"account.follows_you": "É seu seguidor", "account.follows_you": "Segue você",
"account.media": "Mídia", "account.media": "Mídia",
"account.mention": "Mencionar @{name}", "account.mention": "Mencionar @{name}",
"account.mute": "Silenciar @{name}", "account.mute": "Silenciar @{name}",
"account.posts": "Posts", "account.posts": "Posts",
"account.report": "Denunciar @{name}", "account.report": "Denunciar @{name}",
"account.requested": "Aguardando aprovação", "account.requested": "Aguardando aprovação. Clique para cancelar a solicitação.",
"account.share": "Compartilhar perfil de @{name}", "account.share": "Compartilhar perfil de @{name}",
"account.unblock": "Não bloquear @{name}", "account.unblock": "Desbloquear @{name}",
"account.unblock_domain": "Desbloquear {domain}", "account.unblock_domain": "Desbloquear {domain}",
"account.unfollow": "Deixar de seguir", "account.unfollow": "Deixar de seguir",
"account.unmute": "Não silenciar @{name}", "account.unmute": "Não silenciar @{name}",
"account.view_full_profile": "Ver perfil completo", "account.view_full_profile": "Ver perfil completo",
"boost_modal.combo": "Pode clicar {combo} para não voltar a ver", "boost_modal.combo": "Você pode pressionar {combo} para ignorar este diálogo na próxima vez",
"bundle_column_error.body": "Something went wrong while loading this component.", "bundle_column_error.body": "Algo de errado aconteceu enquanto este componente era carregado.",
"bundle_column_error.retry": "Tente novamente", "bundle_column_error.retry": "Tente novamente",
"bundle_column_error.title": "Network error", "bundle_column_error.title": "Erro de rede",
"bundle_modal_error.close": "Fechar", "bundle_modal_error.close": "Fechar",
"bundle_modal_error.message": "Something went wrong while loading this component.", "bundle_modal_error.message": "Algo de errado aconteceu enquanto este componente era carregado.",
"bundle_modal_error.retry": "Tente novamente", "bundle_modal_error.retry": "Tente novamente",
"column.blocks": "Usuários bloqueados", "column.blocks": "Usuários bloqueados",
"column.community": "Local", "column.community": "Local",
@ -33,7 +33,9 @@
"column.home": "Página inicial", "column.home": "Página inicial",
"column.mutes": "Usuários silenciados", "column.mutes": "Usuários silenciados",
"column.notifications": "Notificações", "column.notifications": "Notificações",
"column.pins": "Postagens fixadas",
"column.public": "Global", "column.public": "Global",
"column.pins": "Postagens fixadas",
"column_back_button.label": "Voltar", "column_back_button.label": "Voltar",
"column_header.hide_settings": "Esconder configurações", "column_header.hide_settings": "Esconder configurações",
"column_header.moveLeft_settings": "Mover coluna para a esquerda", "column_header.moveLeft_settings": "Mover coluna para a esquerda",
@ -43,156 +45,169 @@
"column_header.unpin": "Desafixar", "column_header.unpin": "Desafixar",
"column_subheading.navigation": "Navegação", "column_subheading.navigation": "Navegação",
"column_subheading.settings": "Configurações", "column_subheading.settings": "Configurações",
"compose_form.lock_disclaimer": "A sua conta não está {locked}. Qualquer pessoa pode te seguir e visualizar as suas postagens só para seguidores.", "compose_form.lock_disclaimer": "A sua conta não está {locked}. Qualquer pessoa pode te seguir e visualizar postagens direcionadas a apenas seguidores.",
"compose_form.lock_disclaimer.lock": "locked", "compose_form.lock_disclaimer.lock": "trancado",
"compose_form.placeholder": "No que você está pensando?", "compose_form.placeholder": "No que você está pensando?",
"compose_form.privacy_disclaimer": "O seu conteúdo privado será compartilhado com os usuários do {domains}. Você confia {domainsCount, plural, one {neste servidor} other {nestes servidores}}? As configurações de privacidade só funcionam em instâncias do Mastodon. Se {domains} {domainsCount, plural, one {não é uma instância} other {não são instâncias}}, não há como garantir a privacidade de suas postagens, e elas podem ser compartilhadas com outros.", "compose_form.privacy_disclaimer": "O seu conteúdo privado será compartilhado com os usuários de {domains}. Você confia {domainsCount, plural, one {neste servidor} other {nestes servidores}}? As configurações de privacidade só funcionam em instâncias do Mastodon. Se {domains} {domainsCount, plural, one {não é uma instância} other {não são instâncias}}, não há como garantir a privacidade de suas postagens, e elas podem ser compartilhadas com destinatários indesejados.",
"compose_form.publish": "Publicar", "compose_form.publish": "Publicar",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.sensitive": "Marcar mídia como conteúdo sensível", "compose_form.sensitive": "Marcar mídia como conteúdo sensível",
"compose_form.spoiler": "Esconder texto com aviso", "compose_form.spoiler": "Esconder texto com aviso de conteúdo",
"compose_form.spoiler_placeholder": "Aviso de conteúdo", "compose_form.spoiler_placeholder": "Aviso de conteúdo",
"confirmation_modal.cancel": "Cancelar", "confirmation_modal.cancel": "Cancelar",
"confirmations.block.confirm": "Bloquear", "confirmations.block.confirm": "Bloquear",
"confirmations.block.message": "Você tem certeza de que quer bloquear {name}?", "confirmations.block.message": "Você tem certeza de que quer bloquear {name}?",
"confirmations.delete.confirm": "Excluir", "confirmations.delete.confirm": "Excluir",
"confirmations.delete.message": "Você tem certeza de que quer excluir este status?", "confirmations.delete.message": "Você tem certeza de que quer excluir esta postagem?",
"confirmations.domain_block.confirm": "Esconder o domínio inteiro", "confirmations.domain_block.confirm": "Esconder o domínio inteiro",
"confirmations.domain_block.message": "Você quer mesmo bloquear {domain} inteiro? Na maioria dos casos, silenciar ou bloquear alguns usuários é o suficiente e o recomendado.", "confirmations.domain_block.message": "Você quer mesmo bloquear {domain} inteiro? Na maioria dos casos, silenciar ou bloquear alguns usuários é o suficiente e o recomendado.",
"confirmations.mute.confirm": "Silenciar", "confirmations.mute.confirm": "Silenciar",
"confirmations.mute.message": "Você tem certeza de que quer silenciar {name}?", "confirmations.mute.message": "Você tem certeza de que quer silenciar {name}?",
"confirmations.unfollow.confirm": "Deixar de seguir", "confirmations.unfollow.confirm": "Deixar de seguir",
"confirmations.unfollow.message": "Você tem certeza de que quer deixar de seguir {name}?", "confirmations.unfollow.message": "Você tem certeza de que quer deixar de seguir {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "Incorpore esta postagem em seu site copiando o código abaixo:",
"embed.preview": "Here is what it will look like:", "embed.preview": "Aqui está uma previsão de como ficará:",
"emoji_button.activity": "Activity", "emoji_button.activity": "Atividades",
"emoji_button.flags": "Flags", "emoji_button.flags": "Bandeiras",
"emoji_button.food": "Food & Drink", "emoji_button.food": "Comidas & Bebidas",
"emoji_button.label": "Inserir Emoji", "emoji_button.label": "Inserir Emoji",
"emoji_button.nature": "Nature", "emoji_button.nature": "Natureza",
"emoji_button.objects": "Objects", "emoji_button.objects": "Objetos",
"emoji_button.people": "People", "emoji_button.people": "Pessoas",
"emoji_button.search": "Search...", "emoji_button.search": "Buscar...",
"emoji_button.symbols": "Symbols", "emoji_button.symbols": "Símbolos",
"emoji_button.travel": "Travel & Places", "emoji_button.travel": "Viagens & Lugares",
"empty_column.community": "Ainda não existem conteúdo local para mostrar!", "empty_column.community": "A timeline local está vazia. Escreva algo publicamente para começar!",
"empty_column.hashtag": "Ainda não existe qualquer conteúdo com essa hashtag", "empty_column.hashtag": "Ainda não qualquer conteúdo com essa hashtag",
"empty_column.home": "Ainda não segues qualquer utilizador. Visita {public} ou utiliza a pesquisa para procurar outros utilizadores.", "empty_column.home": "Você ainda não segue usuário algo. Visite a timeline {public} ou use o buscador para procurar e conhecer outros usuários.",
"empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.", "empty_column.home.inactivity": "A sua página inicial está vazia. Se você esteve inativo por um tempo, ela irá se regenerar em alguns intantes.",
"empty_column.home.public_timeline": "global", "empty_column.home.public_timeline": "global",
"empty_column.notifications": "Não tens notificações. Interage com outros utilizadores para iniciar uma conversa.", "empty_column.notifications": "Você ainda não possui notificações. Interaja com outros usuários para começar a conversar!",
"empty_column.public": "Não há nada aqui! Escreve algo publicamente ou segue outros utilizadores para ver aqui os conteúdos públicos.", "empty_column.public": "Não há nada aqui! Escreva algo publicamente ou siga manualmente usuários de outras instâncias.",
"follow_request.authorize": "Autorizar", "follow_request.authorize": "Autorizar",
"follow_request.reject": "Rejeitar", "follow_request.reject": "Rejeitar",
"getting_started.appsshort": "Apps", "getting_started.appsshort": "Apps",
"getting_started.faq": "FAQ", "getting_started.faq": "FAQ",
"getting_started.heading": "Primeiros passos", "getting_started.heading": "Primeiros passos",
"getting_started.open_source_notice": "Mastodon é software de fonte aberta. Podes contribuir ou repostar problemas no GitHub do projecto: {github}.", "getting_started.open_source_notice": "Mastodon é um software de código aberto. Você pode contribuir ou reportar problemas na página do GitHub do projeto: {github}.",
"getting_started.userguide": "User Guide", "getting_started.userguide": "Guia de usuário",
"home.column_settings.advanced": "Avançado", "home.column_settings.advanced": "Avançado",
"home.column_settings.basic": "Básico", "home.column_settings.basic": "Básico",
"home.column_settings.filter_regex": "Filtrar com uma expressão regular", "home.column_settings.filter_regex": "Filtrar com uma expressão regular",
"home.column_settings.show_reblogs": "Mostrar as partilhas", "home.column_settings.show_reblogs": "Mostrar compartilhamentos",
"home.column_settings.show_replies": "Mostrar as respostas", "home.column_settings.show_replies": "Mostrar as respostas",
"home.settings": "Parâmetros da listagem", "home.settings": "Configurações de colunas",
"lightbox.close": "Fechar", "lightbox.close": "Fechar",
"lightbox.next": "Next", "lightbox.next": "Próximo",
"lightbox.previous": "Previous", "lightbox.previous": "Anterior",
"loading_indicator.label": "Carregando...", "loading_indicator.label": "Carregando...",
"media_gallery.toggle_visible": "Esconder/Mostrar", "media_gallery.toggle_visible": "Esconder/Mostrar",
"missing_indicator.label": "Não encontrado", "missing_indicator.label": "Não encontrado",
"navigation_bar.blocks": "Utilizadores bloqueados", "navigation_bar.blocks": "Usuários bloqueados",
"navigation_bar.community_timeline": "Local", "navigation_bar.community_timeline": "Local",
"navigation_bar.edit_profile": "Editar perfil", "navigation_bar.edit_profile": "Editar perfil",
"navigation_bar.favourites": "Favoritos", "navigation_bar.favourites": "Favoritos",
"navigation_bar.follow_requests": "Seguidores pendentes", "navigation_bar.follow_requests": "Seguidores pendentes",
"navigation_bar.info": "Mais informações", "navigation_bar.info": "Mais informações",
"navigation_bar.logout": "Sair", "navigation_bar.logout": "Sair",
"navigation_bar.mutes": "Utilizadores silenciados", "navigation_bar.mutes": "Usuários silenciados",
"navigation_bar.pins": "Postagens fixadas",
"navigation_bar.preferences": "Preferências", "navigation_bar.preferences": "Preferências",
"navigation_bar.public_timeline": "Global", "navigation_bar.public_timeline": "Global",
"notification.favourite": "{name} adicionou o teu post aos favoritos", "navigation_bar.preferences": "Preferências",
"notification.follow": "{name} seguiu-te", "navigation_bar.public_timeline": "Global",
"notification.mention": "{name} mencionou-te", "navigation_bar.pins": "Postagens fixadas",
"notification.reblog": "{name} partilhou o teu post", "notification.favourite": "{name} adicionou a sua postagem aos favoritos",
"notification.follow": "{name} te seguiu",
"notification.mention": "{name} te mencionou",
"notification.reblog": "{name} compartilhou a sua postagem",
"notifications.clear": "Limpar notificações", "notifications.clear": "Limpar notificações",
"notifications.clear_confirmation": "Queres mesmo limpar todas as notificações?", "notifications.clear_confirmation": "Você tem certeza de que quer limpar todas as suas notificações permanentemente?",
"notifications.column_settings.alert": "Notificações no computador", "notifications.column_settings.alert": "Notificações no computador",
"notifications.column_settings.favourite": "Favoritos:", "notifications.column_settings.favourite": "Favoritos:",
"notifications.column_settings.follow": "Novos seguidores:", "notifications.column_settings.follow": "Novos seguidores:",
"notifications.column_settings.mention": "Menções:", "notifications.column_settings.mention": "Menções:",
"notifications.column_settings.push": "Push notifications", "notifications.column_settings.push": "Enviar notificações",
"notifications.column_settings.push_meta": "This device", "notifications.column_settings.push_meta": "Este aparelho",
"notifications.column_settings.reblog": "Partilhas:", "notifications.column_settings.reblog": "Compartilhamento:",
"notifications.column_settings.show": "Mostrar nas colunas", "notifications.column_settings.show": "Mostrar nas colunas",
"notifications.column_settings.sound": "Reproduzir som", "notifications.column_settings.sound": "Reproduzir som",
"onboarding.done": "Done", "onboarding.done": "Pronto",
"onboarding.next": "Next", "onboarding.next": "Próximo",
"onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", "onboarding.page_five.public_timelines": "A timeline local mostra postagens públicas de todos os usuários no {domain}. A timeline federada mostra todas as postagens de todas as pessoas que pessoas no {domain} seguem. Estas são as timelines públicas, uma ótima maneira de conhecer novas pessoas.",
"onboarding.page_four.home": "The home timeline shows posts from people you follow.", "onboarding.page_four.home": "A página inicial mostra postagens de pessoas que você segue.",
"onboarding.page_four.notifications": "The notifications column shows when someone interacts with you.", "onboarding.page_four.notifications": "A coluna de notificações te mostra quando alguém interage com você.",
"onboarding.page_one.federation": "Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", "onboarding.page_one.federation": "Mastodon é uma rede d servidores independentes se juntando para fazer uma grande rede social. Nós chamamos estes servidores de instâncias.",
"onboarding.page_one.handle": "You are on {domain}, so your full handle is {handle}", "onboarding.page_one.handle": "Você está no {domain}, então o seu nome de usuário completo é {handle}",
"onboarding.page_one.welcome": "Welcome to Mastodon!", "onboarding.page_one.welcome": "Seja bem-vindo(a) ao Mastodon!",
"onboarding.page_six.admin": "Your instance's admin is {admin}.", "onboarding.page_six.admin": "O administrador de sua instância é {admin}.",
"onboarding.page_six.almost_done": "Almost done...", "onboarding.page_six.almost_done": "Quase acabando...",
"onboarding.page_six.appetoot": "Bon Appetoot!", "onboarding.page_six.appetoot": "Bon Appetoot!",
"onboarding.page_six.apps_available": "There are {apps} available for iOS, Android and other platforms.", "onboarding.page_six.apps_available": "Há {apps} disponíveis para iOS, Android e outras plataformas.",
"onboarding.page_six.github": "Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", "onboarding.page_six.github": "Mastodon é um software gratuito e de código aberto. Você pode reportar bugs, prequisitar novas funções ou contribuir para o código no {github}.",
"onboarding.page_six.guidelines": "community guidelines", "onboarding.page_six.guidelines": "diretrizes da comunidade",
"onboarding.page_six.read_guidelines": "Please read {domain}'s {guidelines}!", "onboarding.page_six.read_guidelines": "Por favor, leia as {guidelines} do {domain}!",
"onboarding.page_six.various_app": "mobile apps", "onboarding.page_six.various_app": "aplicativos móveis",
"onboarding.page_three.profile": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.", "onboarding.page_three.profile": "Edite o seu perfil para mudar o seu o seu avatar, bio e nome de exibição. No menu de configurações, você também encontrará outras preferências.",
"onboarding.page_three.search": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.", "onboarding.page_three.search": "Use a barra de buscas para encontrar pessoas e consultar hashtahs, como #illustrations e #introductions. Para procurar por uma pessoa que não estiver nesta instância, use o nome de usuário completo dela.",
"onboarding.page_two.compose": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.", "onboarding.page_two.compose": "Escreva postagens na coluna de escrita. Você pode hospedar imagens, mudar as configurações de privacidade e adicionar alertas de conteúdo através dos ícones abaixo.",
"onboarding.skip": "Skip", "onboarding.skip": "Pular",
"privacy.change": "Ajustar a privacidade da mensagem", "privacy.change": "Ajustar a privacidade da mensagem",
"privacy.direct.long": "Apenas para utilizadores mencionados", "privacy.direct.long": "Apenas para usuários mencionados",
"privacy.direct.short": "Directo", "privacy.direct.short": "Direta",
"privacy.private.long": "Apenas para os seguidores", "privacy.private.long": "Apenas para seus seguidores",
"privacy.private.short": "Privado", "privacy.private.short": "Privada",
"privacy.public.long": "Publicar em todos os feeds", "privacy.public.long": "Publicar em todos os feeds",
"privacy.public.short": "Público", "privacy.public.short": "Pública",
"privacy.unlisted.long": "Não publicar nos feeds públicos", "privacy.unlisted.long": "Não publicar em feeds públicos",
"privacy.unlisted.short": "Não listar", "privacy.unlisted.short": "Não listada",
"reply_indicator.cancel": "Cancelar", "reply_indicator.cancel": "Cancelar",
"report.placeholder": "Comentários adicionais", "report.placeholder": "Comentários adicionais",
"report.submit": "Enviar", "report.submit": "Enviar",
"report.target": "Denunciar", "report.target": "Denunciar",
"search.placeholder": "Pesquisar", "search.placeholder": "Pesquisar",
"search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}", "search_results.total": "{count, number} {count, plural, one {resultado} other {resultados}}",
"standalone.public_title": "A look inside...", "standalone.public_title": "Dê uma espiada...",
"status.cannot_reblog": "This post cannot be boosted", "status.cannot_reblog": "Esta postagem não pode ser compartilhada",
"status.delete": "Eliminar", "status.delete": "Eliminar",
"status.embed": "Embed", "status.embed": "Incorporar",
"status.favourite": "Adicionar aos favoritos", "status.favourite": "Adicionar aos favoritos",
"status.load_more": "Carregar mais", "status.load_more": "Carregar mais",
"status.media_hidden": "Media escondida", "status.media_hidden": "Mídia escondida",
"status.mention": "Mencionar @{name}", "status.mention": "Mencionar @{name}",
"status.mute_conversation": "Mute conversation", "status.mute_conversation": "Silenciar conversa",
"status.open": "Expandir", "status.open": "Expandir",
"status.pin": "Pin on profile", "status.pin": "Fixar no perfil",
"status.reblog": "Partilhar", "status.reblog": "Compartilhar",
"status.reblogged_by": "{name} partilhou", "status.reblogged_by": "{name} compartilhou",
"status.reply": "Responder", "status.reply": "Responder",
"status.replyAll": "Reply to thread", "status.replyAll": "Responder à sequência",
"status.report": "Denúnciar @{name}", "status.report": "Denunciar @{name}",
"status.sensitive_toggle": "Clique para ver", "status.sensitive_toggle": "Clique para ver",
"status.sensitive_warning": "Conteúdo sensível", "status.sensitive_warning": "Conteúdo sensível",
"status.share": "Share", "status.share": "Compartilhar",
"status.show_less": "Mostrar menos", "status.show_less": "Mostrar menos",
"status.show_more": "Mostrar mais", "status.show_more": "Mostrar mais",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Desativar silêncio desta conversa",
"status.unpin": "Unpin from profile", "status.unpin": "Desafixar do perfil",
"tabs_bar.compose": "Criar", "tabs_bar.compose": "Criar",
"tabs_bar.federated_timeline": "Global", "tabs_bar.federated_timeline": "Global",
"tabs_bar.home": "Home", "tabs_bar.home": "Página inicial",
"tabs_bar.local_timeline": "Local", "tabs_bar.local_timeline": "Local",
"tabs_bar.notifications": "Notificações", "tabs_bar.notifications": "Notificações",
"upload_area.title": "Arraste e solte para enviar", "upload_area.title": "Arraste e solte para enviar",
"upload_button.label": "Adicionar media", "upload_button.label": "Adicionar mídia",
"upload_form.undo": "Anular", "upload_form.undo": "Anular",
"upload_progress.label": "A gravar...", "upload_progress.label": "Salvando...",
"video.close": "Fechar vídeo",
"video.exit_fullscreen": "Sair da tela cheia",
"video.expand": "Expandir vídeo",
"video.fullscreen": "Tela cheia",
"video.hide": "Esconder vídeo",
"video.mute": "Silenciar vídeo",
"video.pause": "Parar",
"video.play": "Reproduzir",
"video.unmute": "Retirar silêncio",
"video_player.expand": "Expandir vídeo", "video_player.expand": "Expandir vídeo",
"video_player.toggle_sound": "Ligar/Desligar som", "video_player.toggle_sound": "Ligar/Desligar som",
"video_player.toggle_visible": "Ligar/Desligar vídeo", "video_player.toggle_visible": "Ligar/Desligar vídeo",

@ -33,6 +33,7 @@
"column.home": "Home", "column.home": "Home",
"column.mutes": "Utilizadores silenciados", "column.mutes": "Utilizadores silenciados",
"column.notifications": "Notificações", "column.notifications": "Notificações",
"column.pins": "Pinned toot",
"column.public": "Global", "column.public": "Global",
"column_back_button.label": "Voltar", "column_back_button.label": "Voltar",
"column_header.hide_settings": "Hide settings", "column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "Mais informações", "navigation_bar.info": "Mais informações",
"navigation_bar.logout": "Sair", "navigation_bar.logout": "Sair",
"navigation_bar.mutes": "Utilizadores silenciados", "navigation_bar.mutes": "Utilizadores silenciados",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Preferências", "navigation_bar.preferences": "Preferências",
"navigation_bar.public_timeline": "Global", "navigation_bar.public_timeline": "Global",
"notification.favourite": "{name} adicionou o teu post aos favoritos", "notification.favourite": "{name} adicionou o teu post aos favoritos",
@ -193,6 +195,15 @@
"upload_button.label": "Adicionar media", "upload_button.label": "Adicionar media",
"upload_form.undo": "Anular", "upload_form.undo": "Anular",
"upload_progress.label": "A gravar...", "upload_progress.label": "A gravar...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Expandir vídeo", "video_player.expand": "Expandir vídeo",
"video_player.toggle_sound": "Ligar/Desligar som", "video_player.toggle_sound": "Ligar/Desligar som",
"video_player.toggle_visible": "Ligar/Desligar vídeo", "video_player.toggle_visible": "Ligar/Desligar vídeo",

@ -33,6 +33,7 @@
"column.home": "Главная", "column.home": "Главная",
"column.mutes": "Список глушения", "column.mutes": "Список глушения",
"column.notifications": "Уведомления", "column.notifications": "Уведомления",
"column.pins": "Pinned toot",
"column.public": "Глобальная лента", "column.public": "Глобальная лента",
"column_back_button.label": "Назад", "column_back_button.label": "Назад",
"column_header.hide_settings": "Скрыть настройки", "column_header.hide_settings": "Скрыть настройки",
@ -109,6 +110,7 @@
"navigation_bar.info": "Об узле", "navigation_bar.info": "Об узле",
"navigation_bar.logout": "Выйти", "navigation_bar.logout": "Выйти",
"navigation_bar.mutes": "Список глушения", "navigation_bar.mutes": "Список глушения",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Опции", "navigation_bar.preferences": "Опции",
"navigation_bar.public_timeline": "Глобальная лента", "navigation_bar.public_timeline": "Глобальная лента",
"notification.favourite": "{name} понравился Ваш статус", "notification.favourite": "{name} понравился Ваш статус",
@ -193,6 +195,15 @@
"upload_button.label": "Добавить медиаконтент", "upload_button.label": "Добавить медиаконтент",
"upload_form.undo": "Отменить", "upload_form.undo": "Отменить",
"upload_progress.label": "Загрузка...", "upload_progress.label": "Загрузка...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Развернуть видео", "video_player.expand": "Развернуть видео",
"video_player.toggle_sound": "Вкл./выкл. звук", "video_player.toggle_sound": "Вкл./выкл. звук",
"video_player.toggle_visible": "Показать/скрыть", "video_player.toggle_visible": "Показать/скрыть",

@ -33,6 +33,7 @@
"column.home": "Home", "column.home": "Home",
"column.mutes": "Muted users", "column.mutes": "Muted users",
"column.notifications": "Notifications", "column.notifications": "Notifications",
"column.pins": "Pinned toot",
"column.public": "Federated timeline", "column.public": "Federated timeline",
"column_back_button.label": "Back", "column_back_button.label": "Back",
"column_header.hide_settings": "Hide settings", "column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "About this instance", "navigation_bar.info": "About this instance",
"navigation_bar.logout": "Logout", "navigation_bar.logout": "Logout",
"navigation_bar.mutes": "Muted users", "navigation_bar.mutes": "Muted users",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Preferences", "navigation_bar.preferences": "Preferences",
"navigation_bar.public_timeline": "Federated timeline", "navigation_bar.public_timeline": "Federated timeline",
"notification.favourite": "{name} favourited your status", "notification.favourite": "{name} favourited your status",
@ -193,6 +195,15 @@
"upload_button.label": "Add media", "upload_button.label": "Add media",
"upload_form.undo": "Undo", "upload_form.undo": "Undo",
"upload_progress.label": "Uploading...", "upload_progress.label": "Uploading...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Expand video", "video_player.expand": "Expand video",
"video_player.toggle_sound": "Toggle sound", "video_player.toggle_sound": "Toggle sound",
"video_player.toggle_visible": "Toggle visibility", "video_player.toggle_visible": "Toggle visibility",

@ -33,6 +33,7 @@
"column.home": "Anasayfa", "column.home": "Anasayfa",
"column.mutes": "Susturulmuş kullanıcılar", "column.mutes": "Susturulmuş kullanıcılar",
"column.notifications": "Bildirimler", "column.notifications": "Bildirimler",
"column.pins": "Pinned toot",
"column.public": "Federe zaman tüneli", "column.public": "Federe zaman tüneli",
"column_back_button.label": "Geri", "column_back_button.label": "Geri",
"column_header.hide_settings": "Hide settings", "column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "Genişletilmiş bilgi", "navigation_bar.info": "Genişletilmiş bilgi",
"navigation_bar.logout": ıkış", "navigation_bar.logout": ıkış",
"navigation_bar.mutes": "Sessize alınmış kullanıcılar", "navigation_bar.mutes": "Sessize alınmış kullanıcılar",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Tercihler", "navigation_bar.preferences": "Tercihler",
"navigation_bar.public_timeline": "Federe zaman tüneli", "navigation_bar.public_timeline": "Federe zaman tüneli",
"notification.favourite": "{name} senin durumunu favorilere ekledi", "notification.favourite": "{name} senin durumunu favorilere ekledi",
@ -193,6 +195,15 @@
"upload_button.label": "Görsel ekle", "upload_button.label": "Görsel ekle",
"upload_form.undo": "Geri al", "upload_form.undo": "Geri al",
"upload_progress.label": "Yükleniyor...", "upload_progress.label": "Yükleniyor...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Videoyu genişlet", "video_player.expand": "Videoyu genişlet",
"video_player.toggle_sound": "Sesi aç/kapa", "video_player.toggle_sound": "Sesi aç/kapa",
"video_player.toggle_visible": "Göster/gizle", "video_player.toggle_visible": "Göster/gizle",

@ -33,6 +33,7 @@
"column.home": "Головна", "column.home": "Головна",
"column.mutes": "Заглушені користувачі", "column.mutes": "Заглушені користувачі",
"column.notifications": "Сповіщення", "column.notifications": "Сповіщення",
"column.pins": "Pinned toot",
"column.public": "Глобальна стрічка", "column.public": "Глобальна стрічка",
"column_back_button.label": "Назад", "column_back_button.label": "Назад",
"column_header.hide_settings": "Hide settings", "column_header.hide_settings": "Hide settings",
@ -109,6 +110,7 @@
"navigation_bar.info": "Про інстанцію", "navigation_bar.info": "Про інстанцію",
"navigation_bar.logout": "Вийти", "navigation_bar.logout": "Вийти",
"navigation_bar.mutes": "Заглушені користувачі", "navigation_bar.mutes": "Заглушені користувачі",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.preferences": "Налаштування", "navigation_bar.preferences": "Налаштування",
"navigation_bar.public_timeline": "Глобальна стрічка", "navigation_bar.public_timeline": "Глобальна стрічка",
"notification.favourite": "{name} сподобався ваш допис", "notification.favourite": "{name} сподобався ваш допис",
@ -193,6 +195,15 @@
"upload_button.label": "Додати медіаконтент", "upload_button.label": "Додати медіаконтент",
"upload_form.undo": "Відмінити", "upload_form.undo": "Відмінити",
"upload_progress.label": "Завантаження...", "upload_progress.label": "Завантаження...",
"video.close": "Close video",
"video.exit_fullscreen": "Exit full screen",
"video.expand": "Expand video",
"video.fullscreen": "Full screen",
"video.hide": "Hide video",
"video.mute": "Mute sound",
"video.pause": "Pause",
"video.play": "Play",
"video.unmute": "Unmute sound",
"video_player.expand": "Розгорнути ", "video_player.expand": "Розгорнути ",
"video_player.toggle_sound": "Увімкнути/вимкнути звук", "video_player.toggle_sound": "Увімкнути/вимкнути звук",
"video_player.toggle_visible": "Показати/приховати", "video_player.toggle_visible": "Показати/приховати",

@ -1,13 +1,13 @@
{ {
"account.block": "屏蔽 @{name}", "account.block": "屏蔽 @{name}",
"account.block_domain": "Hide everything from {domain}", "account.block_domain": "隐藏一切来自 {domain} 的嘟文",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.", "account.disclaimer_full": "下列资料不一定完整。",
"account.edit_profile": "修改个人资料", "account.edit_profile": "修改个人资料",
"account.follow": "关注", "account.follow": "关注",
"account.followers": "关注者", "account.followers": "关注者",
"account.follows": "正关注", "account.follows": "正关注",
"account.follows_you": "关注你", "account.follows_you": "关注你",
"account.media": "Media", "account.media": "媒体",
"account.mention": "提及 @{name}", "account.mention": "提及 @{name}",
"account.mute": "将 @{name} 静音", "account.mute": "将 @{name} 静音",
"account.posts": "嘟文", "account.posts": "嘟文",
@ -15,40 +15,41 @@
"account.requested": "等待审批", "account.requested": "等待审批",
"account.share": "分享 @{name}的个人资料", "account.share": "分享 @{name}的个人资料",
"account.unblock": "解除对 @{name} 的屏蔽", "account.unblock": "解除对 @{name} 的屏蔽",
"account.unblock_domain": "解除封锁 {domain}", "account.unblock_domain": "不再隐藏 {domain}",
"account.unfollow": "取消关注", "account.unfollow": "取消关注",
"account.unmute": "取消 @{name} 的静音", "account.unmute": "取消 @{name} 的静音",
"account.view_full_profile": "查看完整资料", "account.view_full_profile": "查看完整资料",
"boost_modal.combo": "如你想在下次路过时显示,请按{combo}", "boost_modal.combo": "如你想在下次路过时显示,请按{combo}",
"bundle_column_error.body": "载入组件出错。", "bundle_column_error.body": "载入组件出错。",
"bundle_column_error.retry": "再次尝试", "bundle_column_error.retry": "试",
"bundle_column_error.title": "网络错误", "bundle_column_error.title": "网络错误",
"bundle_modal_error.close": "关闭", "bundle_modal_error.close": "关闭",
"bundle_modal_error.message": "载入组件出错。", "bundle_modal_error.message": "载入组件出错。",
"bundle_modal_error.retry": "再次尝试", "bundle_modal_error.retry": "试",
"column.blocks": "屏蔽用户", "column.blocks": "屏蔽用户",
"column.community": "本站时间轴", "column.community": "本站时间轴",
"column.favourites": "过的嘟文", "column.favourites": "收藏过的嘟文",
"column.follow_requests": "关注请求", "column.follow_requests": "关注请求",
"column.home": "主页", "column.home": "主页",
"column.mutes": "被静音的用户", "column.mutes": "被静音的用户",
"column.notifications": "通知", "column.notifications": "通知",
"column.pins": "Pinned toot",
"column.public": "跨站公共时间轴", "column.public": "跨站公共时间轴",
"column_back_button.label": "返回", "column_back_button.label": "返回",
"column_header.hide_settings": "Hide settings", "column_header.hide_settings": "隐藏设置",
"column_header.moveLeft_settings": "Move column to the left", "column_header.moveLeft_settings": "将栏左移",
"column_header.moveRight_settings": "Move column to the right", "column_header.moveRight_settings": "将栏右移",
"column_header.pin": "Pin", "column_header.pin": "置顶",
"column_header.show_settings": "Show settings", "column_header.show_settings": "显示设置",
"column_header.unpin": "Unpin", "column_header.unpin": "撤顶",
"column_subheading.navigation": "导航", "column_subheading.navigation": "导航",
"column_subheading.settings": "设置", "column_subheading.settings": "设置",
"compose_form.lock_disclaimer": "你的户没 {locked}. 任何人可以通过关注你来查看只有关注者可见的嘟文.", "compose_form.lock_disclaimer": "你的户没 {locked}. 任何人可以通过关注你来查看只有关注者可见的嘟文.",
"compose_form.lock_disclaimer.lock": "被保护", "compose_form.lock_disclaimer.lock": "被保护",
"compose_form.placeholder": "在想啥?", "compose_form.placeholder": "在想啥?",
"compose_form.privacy_disclaimer": "你的私人嘟文,将被发送至你所提及的 {domains} 用户。你是否信任{domainsCount, plural, one {这个网站} other {这些网站}}?请留意,嘟文隐私设置只适用于各 Mastodon 服务器实例,如果 {domains} {domainsCount, plural, one {不是 Mastodon 服务器实例} other {之中有些不是 Mastodon 服务器实例}},对方将无法收到这篇嘟文的隐私设置,然后可能被转嘟给不能预知的用户阅读。", "compose_form.privacy_disclaimer": "你的私人嘟文,将被发送至你所提及的 {domains} 用户。你是否信任{domainsCount, plural, one {这个网站} other {这些网站}}?请留意,嘟文隐私设置只适用于各 Mastodon 服务器实例,如果 {domains} {domainsCount, plural, one {不是 Mastodon 服务器实例} other {之中有些不是 Mastodon 服务器实例}},对方将无法收到这篇嘟文的隐私设置,然后可能被转嘟给不能预知的用户阅读。",
"compose_form.publish": "嘟嘟", "compose_form.publish": "嘟嘟",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}",
"compose_form.sensitive": "将媒体文件标示为“敏感内容”", "compose_form.sensitive": "将媒体文件标示为“敏感内容”",
"compose_form.spoiler": "将部分文本藏于警告消息之后", "compose_form.spoiler": "将部分文本藏于警告消息之后",
"compose_form.spoiler_placeholder": "敏感内容的警告消息", "compose_form.spoiler_placeholder": "敏感内容的警告消息",
@ -57,14 +58,14 @@
"confirmations.block.message": "想好了,真的要屏蔽 {name}?", "confirmations.block.message": "想好了,真的要屏蔽 {name}?",
"confirmations.delete.confirm": "删除", "confirmations.delete.confirm": "删除",
"confirmations.delete.message": "想好了,真的要删除这条嘟文?", "confirmations.delete.message": "想好了,真的要删除这条嘟文?",
"confirmations.domain_block.confirm": "Hide entire domain", "confirmations.domain_block.confirm": "隐藏整个网站",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.", "confirmations.domain_block.message": "你真的真的确定要隐藏整个 {domain} ?多数情况下,封锁或静音几个特定目标就好。",
"confirmations.mute.confirm": "静音", "confirmations.mute.confirm": "静音",
"confirmations.mute.message": "想好了,真的要静音 {name}?", "confirmations.mute.message": "想好了,真的要静音 {name}?",
"confirmations.unfollow.confirm": "取消关注", "confirmations.unfollow.confirm": "取消关注",
"confirmations.unfollow.message": "确定要取消关注 {name}吗?", "confirmations.unfollow.message": "确定要取消关注 {name}吗?",
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "要内嵌此嘟文,请将以下代码贴进你的网站。",
"embed.preview": "Here is what it will look like:", "embed.preview": "到时大概长这样:",
"emoji_button.activity": "活动", "emoji_button.activity": "活动",
"emoji_button.flags": "旗帜", "emoji_button.flags": "旗帜",
"emoji_button.food": "食物和饮料", "emoji_button.food": "食物和饮料",
@ -72,13 +73,13 @@
"emoji_button.nature": "自然", "emoji_button.nature": "自然",
"emoji_button.objects": "物体", "emoji_button.objects": "物体",
"emoji_button.people": "人物", "emoji_button.people": "人物",
"emoji_button.search": "搜索...", "emoji_button.search": "搜索",
"emoji_button.symbols": "符号", "emoji_button.symbols": "符号",
"emoji_button.travel": "旅途和地点", "emoji_button.travel": "旅途和地点",
"empty_column.community": "本站时间轴暂时未有内容,快贴文来抢头香啊!", "empty_column.community": "本站时间轴暂时未有内容,快嘟几个来抢头香啊!",
"empty_column.hashtag": "这个标签暂时未有内容。", "empty_column.hashtag": "这个标签暂时未有内容。",
"empty_column.home": "你还没有关注任何用户。快看看{public},向其他用户搭讪吧。", "empty_column.home": "你还没有关注任何用户。快看看{public},向其他用户搭讪吧。",
"empty_column.home.inactivity": "Your home feed is empty. If you have been inactive for a while, it will be regenerated for you soon.", "empty_column.home.inactivity": "你的主页暂时没有内容。也许你太久没有来了?如果是这样,文章会慢慢出来,请稍后再看。",
"empty_column.home.public_timeline": "公共时间轴", "empty_column.home.public_timeline": "公共时间轴",
"empty_column.notifications": "你没有任何通知纪录,快向其他用户搭讪吧。", "empty_column.notifications": "你没有任何通知纪录,快向其他用户搭讪吧。",
"empty_column.public": "跨站公共时间轴暂时没有内容!快写一些公共的嘟文,或者关注另一些服务器实例的用户吧!你和本站、友站的交流,将决定这里出现的内容。", "empty_column.public": "跨站公共时间轴暂时没有内容!快写一些公共的嘟文,或者关注另一些服务器实例的用户吧!你和本站、友站的交流,将决定这里出现的内容。",
@ -96,33 +97,34 @@
"home.column_settings.show_replies": "显示回应嘟文", "home.column_settings.show_replies": "显示回应嘟文",
"home.settings": "字段设置", "home.settings": "字段设置",
"lightbox.close": "关闭", "lightbox.close": "关闭",
"lightbox.next": "Next", "lightbox.next": "下一步",
"lightbox.previous": "Previous", "lightbox.previous": "上一步",
"loading_indicator.label": "加载中……", "loading_indicator.label": "加载中……",
"media_gallery.toggle_visible": "打开或关上", "media_gallery.toggle_visible": "打开或关上",
"missing_indicator.label": "找不到内容", "missing_indicator.label": "找不到内容",
"navigation_bar.blocks": "被屏蔽的用户", "navigation_bar.blocks": "被屏蔽的用户",
"navigation_bar.community_timeline": "本站时间轴", "navigation_bar.community_timeline": "本站时间轴",
"navigation_bar.edit_profile": "修改个人资料", "navigation_bar.edit_profile": "修改个人资料",
"navigation_bar.favourites": "的内容", "navigation_bar.favourites": "收藏的内容",
"navigation_bar.follow_requests": "关注请求", "navigation_bar.follow_requests": "关注请求",
"navigation_bar.info": "关于本站", "navigation_bar.info": "关于本站",
"navigation_bar.logout": "注销", "navigation_bar.logout": "注销",
"navigation_bar.mutes": "被静音的用户", "navigation_bar.mutes": "被静音的用户",
"navigation_bar.pins": "置顶嘟文",
"navigation_bar.preferences": "首选项", "navigation_bar.preferences": "首选项",
"navigation_bar.public_timeline": "跨站公共时间轴", "navigation_bar.public_timeline": "跨站公共时间轴",
"notification.favourite": "{name} 了你的嘟文", "notification.favourite": "{name} 收藏了你的嘟文",
"notification.follow": "{name} 开始关注你", "notification.follow": "{name} 开始关注你",
"notification.mention": "{name} 提及你", "notification.mention": "{name} 提及你",
"notification.reblog": "{name} 转嘟了你的嘟文", "notification.reblog": "{name} 转嘟了你的嘟文",
"notifications.clear": "清空通知纪录", "notifications.clear": "清空通知纪录",
"notifications.clear_confirmation": "你确定要清空通知纪录吗?", "notifications.clear_confirmation": "你确定要清空通知纪录吗?",
"notifications.column_settings.alert": "显示桌面通知", "notifications.column_settings.alert": "显示桌面通知",
"notifications.column_settings.favourite": "你的嘟文被", "notifications.column_settings.favourite": "你的嘟文被收藏",
"notifications.column_settings.follow": "关注你:", "notifications.column_settings.follow": "关注你:",
"notifications.column_settings.mention": "提及你:", "notifications.column_settings.mention": "提及你:",
"notifications.column_settings.push": "Push notifications", "notifications.column_settings.push": "推送通知",
"notifications.column_settings.push_meta": "This device", "notifications.column_settings.push_meta": "此设备",
"notifications.column_settings.reblog": "你的嘟文被转嘟:", "notifications.column_settings.reblog": "你的嘟文被转嘟:",
"notifications.column_settings.show": "在通知栏显示", "notifications.column_settings.show": "在通知栏显示",
"notifications.column_settings.sound": "播放音效", "notifications.column_settings.sound": "播放音效",
@ -132,18 +134,18 @@
"onboarding.page_four.home": "你的主时间轴上是你关注的用户的嘟文.", "onboarding.page_four.home": "你的主时间轴上是你关注的用户的嘟文.",
"onboarding.page_four.notifications": "如果你和他人产生了互动,便会出现在通知列上啦~", "onboarding.page_four.notifications": "如果你和他人产生了互动,便会出现在通知列上啦~",
"onboarding.page_one.federation": "Mastodon是由一系列独立的服务器共同打造的强大的社交网络我们将这些独立但又相互连接的服务器叫做服务器实例。", "onboarding.page_one.federation": "Mastodon是由一系列独立的服务器共同打造的强大的社交网络我们将这些独立但又相互连接的服务器叫做服务器实例。",
"onboarding.page_one.handle": "你在 {domain}, {handle} 就是你的完整户名称。", "onboarding.page_one.handle": "你在 {domain}, {handle} 就是你的完整户名称。",
"onboarding.page_one.welcome": "欢迎来到 Mastodon!", "onboarding.page_one.welcome": "欢迎来到 Mastodon!",
"onboarding.page_six.admin": "{admin} 是你所在服务器实例的管理员.", "onboarding.page_six.admin": "{admin} 是你所在服务器实例的管理员.",
"onboarding.page_six.almost_done": "快完成了...", "onboarding.page_six.almost_done": "差不多了…",
"onboarding.page_six.appetoot": "嗷呜~", "onboarding.page_six.appetoot": "嗷呜~",
"onboarding.page_six.apps_available": "也有适用于 iOS, Android 和其它平台的 {apps} 咯~", "onboarding.page_six.apps_available": "也有适用于 iOS, Android 和其它平台的 {apps} 咯~",
"onboarding.page_six.github": "Mastodon 是自由的开放源代码软件。欢迎来 {github} 报告问题,提交功能请求,或者贡献代码 :-)", "onboarding.page_six.github": "Mastodon 是自由的开放源代码软件。欢迎来 {github} 报告问题,提交功能请求,或者贡献代码 :-)",
"onboarding.page_six.guidelines": "社区指南", "onboarding.page_six.guidelines": "社区指南",
"onboarding.page_six.read_guidelines": "别忘了看看 {domain} 的 {guidelines}!", "onboarding.page_six.read_guidelines": "别忘了看看 {domain} 的 {guidelines}",
"onboarding.page_six.various_app": "移动应用程序", "onboarding.page_six.various_app": "移动应用程序",
"onboarding.page_three.profile": "修改你的个人资料,比如头像、简介、和昵称等等。在那还可以找到其它首选项。", "onboarding.page_three.profile": "修改你的个人资料,比如头像、简介、和昵称等等。在那还可以找到其它首选项。",
"onboarding.page_three.search": "用搜索来找人和标签吧,比如 {illustration} 或者 {introductions}。想找其它服务器实例上的人,用完整户名称(用户名@域名)啦。", "onboarding.page_three.search": "用搜索来找人和标签吧,比如 {illustration} 或者 {introductions}。想找其它服务器实例上的人,用完整户名称(用户名@域名)啦。",
"onboarding.page_two.compose": "从这里开始嘟!上面的按钮提供了上传图片,修改隐私设置和提示敏感内容等多种功能。.", "onboarding.page_two.compose": "从这里开始嘟!上面的按钮提供了上传图片,修改隐私设置和提示敏感内容等多种功能。.",
"onboarding.skip": "好啦好啦我知道啦", "onboarding.skip": "好啦好啦我知道啦",
"privacy.change": "调整隐私设置", "privacy.change": "调整隐私设置",
@ -161,29 +163,29 @@
"report.target": "Reporting", "report.target": "Reporting",
"search.placeholder": "搜索", "search.placeholder": "搜索",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}", "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"standalone.public_title": "A look inside...", "standalone.public_title": "大家都在干啥?",
"status.cannot_reblog": "没法转嘟这条嘟文啦……", "status.cannot_reblog": "没法转嘟这条嘟文啦……",
"status.delete": "删除", "status.delete": "删除",
"status.embed": "Embed", "status.embed": "嵌入",
"status.favourite": "", "status.favourite": "收藏",
"status.load_more": "加载更多", "status.load_more": "加载更多",
"status.media_hidden": "隐藏媒体内容", "status.media_hidden": "隐藏媒体内容",
"status.mention": "提及 @{name}", "status.mention": "提及 @{name}",
"status.mute_conversation": "Mute conversation", "status.mute_conversation": "静音对话",
"status.open": "展开嘟文", "status.open": "展开嘟文",
"status.pin": "Pin on profile", "status.pin": "置顶到资料",
"status.reblog": "转嘟", "status.reblog": "转嘟",
"status.reblogged_by": "{name} 转嘟", "status.reblogged_by": "{name} 转嘟",
"status.reply": "回应", "status.reply": "回应",
"status.replyAll": "Reply to thread", "status.replyAll": "回应整串",
"status.report": "举报 @{name}", "status.report": "举报 @{name}",
"status.sensitive_toggle": "点击显示", "status.sensitive_toggle": "点击显示",
"status.sensitive_warning": "敏感内容", "status.sensitive_warning": "敏感内容",
"status.share": "Share", "status.share": "Share",
"status.show_less": "减少显示", "status.show_less": "减少显示",
"status.show_more": "显示更多", "status.show_more": "显示更多",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "解禁对话",
"status.unpin": "Unpin from profile", "status.unpin": "解除置顶",
"tabs_bar.compose": "撰写", "tabs_bar.compose": "撰写",
"tabs_bar.federated_timeline": "跨站", "tabs_bar.federated_timeline": "跨站",
"tabs_bar.home": "主页", "tabs_bar.home": "主页",
@ -193,6 +195,15 @@
"upload_button.label": "上传媒体文件", "upload_button.label": "上传媒体文件",
"upload_form.undo": "还原", "upload_form.undo": "还原",
"upload_progress.label": "上传中……", "upload_progress.label": "上传中……",
"video.close": "关闭影片",
"video.exit_fullscreen": "退出全荧幕",
"video.expand": "展开影片",
"video.fullscreen": "全荧幕",
"video.hide": "隐藏影片",
"video.mute": "静音",
"video.pause": "暂停",
"video.play": "播放",
"video.unmute": "解除静音",
"video_player.expand": "展开影片", "video_player.expand": "展开影片",
"video_player.toggle_sound": "开关音效", "video_player.toggle_sound": "开关音效",
"video_player.toggle_visible": "打开或关上", "video_player.toggle_visible": "打开或关上",

@ -1,46 +1,47 @@
{ {
"account.block": "封鎖 @{name}", "account.block": "封鎖 @{name}",
"account.block_domain": "Hide everything from {domain}", "account.block_domain": "隱藏來自 {domain} 的一切文章",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.", "account.disclaimer_full": "下列資料不一定完整。",
"account.edit_profile": "修改個人資料", "account.edit_profile": "修改個人資料",
"account.follow": "關注", "account.follow": "關注",
"account.followers": "關注的人", "account.followers": "關注的人",
"account.follows": "正關注", "account.follows": "正關注",
"account.follows_you": "關注你", "account.follows_you": "關注你",
"account.media": "Media", "account.media": "媒體",
"account.mention": "提及 @{name}", "account.mention": "提及 @{name}",
"account.mute": "將 @{name} 靜音", "account.mute": "將 @{name} 靜音",
"account.posts": "文章", "account.posts": "文章",
"account.report": "舉報 @{name}", "account.report": "舉報 @{name}",
"account.requested": "等候審批", "account.requested": "等候審批",
"account.share": "Share @{name}'s profile", "account.share": "分享 @{name} 的個人資料",
"account.unblock": "解除對 @{name} 的封鎖", "account.unblock": "解除對 @{name} 的封鎖",
"account.unblock_domain": "Unhide {domain}", "account.unblock_domain": "不再隱藏 {domain}",
"account.unfollow": "取消關注", "account.unfollow": "取消關注",
"account.unmute": "取消 @{name} 的靜音", "account.unmute": "取消 @{name} 的靜音",
"account.view_full_profile": "View full profile", "account.view_full_profile": "查看完整資料",
"boost_modal.combo": "如你想在下次路過這顯示,請按{combo}", "boost_modal.combo": "如你想在下次路過這顯示,請按{combo}",
"bundle_column_error.body": "Something went wrong while loading this component.", "bundle_column_error.body": "加載本組件出錯。",
"bundle_column_error.retry": "Try again", "bundle_column_error.retry": "重試",
"bundle_column_error.title": "Network error", "bundle_column_error.title": "網絡錯誤",
"bundle_modal_error.close": "Close", "bundle_modal_error.close": "關閉",
"bundle_modal_error.message": "Something went wrong while loading this component.", "bundle_modal_error.message": "加載本組件出錯。",
"bundle_modal_error.retry": "Try again", "bundle_modal_error.retry": "重試",
"column.blocks": "封鎖用戶", "column.blocks": "封鎖用戶",
"column.community": "本站時間軸", "column.community": "本站時間軸",
"column.favourites": "喜歡的文章", "column.favourites": "最愛的文章",
"column.follow_requests": "關注請求", "column.follow_requests": "關注請求",
"column.home": "主頁", "column.home": "主頁",
"column.mutes": "靜音名單", "column.mutes": "靜音名單",
"column.notifications": "通知", "column.notifications": "通知",
"column.pins": "Pinned toot",
"column.public": "跨站時間軸", "column.public": "跨站時間軸",
"column_back_button.label": "返回", "column_back_button.label": "返回",
"column_header.hide_settings": "Hide settings", "column_header.hide_settings": "隱藏設定",
"column_header.moveLeft_settings": "Move column to the left", "column_header.moveLeft_settings": "將欄左移",
"column_header.moveRight_settings": "Move column to the right", "column_header.moveRight_settings": "將欄右移",
"column_header.pin": "Pin", "column_header.pin": "置頂",
"column_header.show_settings": "Show settings", "column_header.show_settings": "顯示設定",
"column_header.unpin": "Unpin", "column_header.unpin": "撤頂",
"column_subheading.navigation": "瀏覽", "column_subheading.navigation": "瀏覽",
"column_subheading.settings": "設定", "column_subheading.settings": "設定",
"compose_form.lock_disclaimer": "你的用戶狀態為「{locked}」,任何人都能立即關注你,然後看到「只有關注者能看」的文章。", "compose_form.lock_disclaimer": "你的用戶狀態為「{locked}」,任何人都能立即關注你,然後看到「只有關注者能看」的文章。",
@ -48,7 +49,7 @@
"compose_form.placeholder": "你在想甚麼?", "compose_form.placeholder": "你在想甚麼?",
"compose_form.privacy_disclaimer": "你的私人文章,將被遞送至 {domains}。你是否信任{domainsCount, plural, one {這個網站} other {這些網站}}?請留意,文章私隱設定只適用於 Mastodon 服務站,如果 {domains} {domainsCount, plural, one {不是 Mastodon 服務站} other {之中有些不是 Mastodon 服務站}},對方將可無視文章的私隱設定,轉推文章給其他用戶閱讀。", "compose_form.privacy_disclaimer": "你的私人文章,將被遞送至 {domains}。你是否信任{domainsCount, plural, one {這個網站} other {這些網站}}?請留意,文章私隱設定只適用於 Mastodon 服務站,如果 {domains} {domainsCount, plural, one {不是 Mastodon 服務站} other {之中有些不是 Mastodon 服務站}},對方將可無視文章的私隱設定,轉推文章給其他用戶閱讀。",
"compose_form.publish": "發文", "compose_form.publish": "發文",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}",
"compose_form.sensitive": "將媒體檔案標示為「敏感內容」", "compose_form.sensitive": "將媒體檔案標示為「敏感內容」",
"compose_form.spoiler": "將部份文字藏於警告訊息之後", "compose_form.spoiler": "將部份文字藏於警告訊息之後",
"compose_form.spoiler_placeholder": "敏感警告訊息", "compose_form.spoiler_placeholder": "敏感警告訊息",
@ -57,14 +58,14 @@
"confirmations.block.message": "你確定要封鎖{name}嗎?", "confirmations.block.message": "你確定要封鎖{name}嗎?",
"confirmations.delete.confirm": "刪除", "confirmations.delete.confirm": "刪除",
"confirmations.delete.message": "你確定要刪除{name}嗎?", "confirmations.delete.message": "你確定要刪除{name}嗎?",
"confirmations.domain_block.confirm": "Hide entire domain", "confirmations.domain_block.confirm": "隱藏整個網站",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.", "confirmations.domain_block.message": "你真的真的確定要隱藏整個 {domain} ?多數情況下,比較推薦封鎖或靜音幾個特定目標就好。",
"confirmations.mute.confirm": "靜音", "confirmations.mute.confirm": "靜音",
"confirmations.mute.message": "你確定要將{name}靜音嗎?", "confirmations.mute.message": "你確定要將{name}靜音嗎?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "取消關注",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "真的不要繼續關注 {name} 了嗎?",
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "要內嵌此文章,請將以下代碼貼進你的網站。",
"embed.preview": "Here is what it will look like:", "embed.preview": "看上去會是這樣:",
"emoji_button.activity": "活動", "emoji_button.activity": "活動",
"emoji_button.flags": "旗幟", "emoji_button.flags": "旗幟",
"emoji_button.food": "飲飲食食", "emoji_button.food": "飲飲食食",
@ -75,7 +76,7 @@
"emoji_button.search": "搜尋…", "emoji_button.search": "搜尋…",
"emoji_button.symbols": "符號", "emoji_button.symbols": "符號",
"emoji_button.travel": "旅遊景物", "emoji_button.travel": "旅遊景物",
"empty_column.community": "本站時間軸暫時未有內容,快文來搶頭香啊!", "empty_column.community": "本站時間軸暫時未有內容,快來搶頭香啊!",
"empty_column.hashtag": "這個標籤暫時未有內容。", "empty_column.hashtag": "這個標籤暫時未有內容。",
"empty_column.home": "你還沒有關注任何用戶。快看看{public},向其他用戶搭訕吧。", "empty_column.home": "你還沒有關注任何用戶。快看看{public},向其他用戶搭訕吧。",
"empty_column.home.inactivity": "你的主頁暫時沒有內容。也許你太久沒有來?如果是這樣,文章會慢慢出來,請稍後再看。", "empty_column.home.inactivity": "你的主頁暫時沒有內容。也許你太久沒有來?如果是這樣,文章會慢慢出來,請稍後再看。",
@ -96,34 +97,35 @@
"home.column_settings.show_replies": "顯示回應文章", "home.column_settings.show_replies": "顯示回應文章",
"home.settings": "欄位設定", "home.settings": "欄位設定",
"lightbox.close": "關閉", "lightbox.close": "關閉",
"lightbox.next": "Next", "lightbox.next": "繼續",
"lightbox.previous": "Previous", "lightbox.previous": "回退",
"loading_indicator.label": "載入中...", "loading_indicator.label": "載入中...",
"media_gallery.toggle_visible": "打開或關上", "media_gallery.toggle_visible": "打開或關上",
"missing_indicator.label": "找不到內容", "missing_indicator.label": "找不到內容",
"navigation_bar.blocks": "被你封鎖的用戶", "navigation_bar.blocks": "被你封鎖的用戶",
"navigation_bar.community_timeline": "本站時間軸", "navigation_bar.community_timeline": "本站時間軸",
"navigation_bar.edit_profile": "修改個人資料", "navigation_bar.edit_profile": "修改個人資料",
"navigation_bar.favourites": "喜歡的內容", "navigation_bar.favourites": "最愛的內容",
"navigation_bar.follow_requests": "關注請求", "navigation_bar.follow_requests": "關注請求",
"navigation_bar.info": "關於本服務站", "navigation_bar.info": "關於本服務站",
"navigation_bar.logout": "登出", "navigation_bar.logout": "登出",
"navigation_bar.mutes": "被你靜音的用戶", "navigation_bar.mutes": "被你靜音的用戶",
"navigation_bar.pins": "置頂文章",
"navigation_bar.preferences": "偏好設定", "navigation_bar.preferences": "偏好設定",
"navigation_bar.public_timeline": "跨站時間軸", "navigation_bar.public_timeline": "跨站時間軸",
"notification.favourite": "{name} 喜歡你的文章", "notification.favourite": "{name} 收藏了你的文章",
"notification.follow": "{name} 開始關注你", "notification.follow": "{name} 開始關注你",
"notification.mention": "{name} 提及你", "notification.mention": "{name} 提及你",
"notification.reblog": "{name} 轉推你的文章", "notification.reblog": "{name} 轉推你的文章",
"notifications.clear": "清空通知紀錄", "notifications.clear": "清空通知紀錄",
"notifications.clear_confirmation": "你確定要清空通知紀錄嗎?", "notifications.clear_confirmation": "你確定要清空通知紀錄嗎?",
"notifications.column_settings.alert": "顯示桌面通知", "notifications.column_settings.alert": "顯示桌面通知",
"notifications.column_settings.favourite": "喜歡你的文章:", "notifications.column_settings.favourite": "收藏了你的文章:",
"notifications.column_settings.follow": "關注你:", "notifications.column_settings.follow": "關注你",
"notifications.column_settings.mention": "提及你:", "notifications.column_settings.mention": "提及你",
"notifications.column_settings.push": "Push notifications", "notifications.column_settings.push": "推送通知",
"notifications.column_settings.push_meta": "This device", "notifications.column_settings.push_meta": "這臺設備",
"notifications.column_settings.reblog": "轉推你的文章:", "notifications.column_settings.reblog": "轉推你的文章",
"notifications.column_settings.show": "在通知欄顯示", "notifications.column_settings.show": "在通知欄顯示",
"notifications.column_settings.sound": "播放音效", "notifications.column_settings.sound": "播放音效",
"onboarding.done": "開始使用", "onboarding.done": "開始使用",
@ -161,17 +163,17 @@
"report.target": "舉報", "report.target": "舉報",
"search.placeholder": "搜尋", "search.placeholder": "搜尋",
"search_results.total": "{count, number} 項結果", "search_results.total": "{count, number} 項結果",
"standalone.public_title": "A look inside...", "standalone.public_title": "站點一瞥…",
"status.cannot_reblog": "這篇文章無法被轉推", "status.cannot_reblog": "這篇文章無法被轉推",
"status.delete": "刪除", "status.delete": "刪除",
"status.embed": "Embed", "status.embed": "鑲嵌",
"status.favourite": "喜歡", "status.favourite": "收藏",
"status.load_more": "載入更多", "status.load_more": "載入更多",
"status.media_hidden": "隱藏媒體內容", "status.media_hidden": "隱藏媒體內容",
"status.mention": "提及 @{name}", "status.mention": "提及 @{name}",
"status.mute_conversation": "Mute conversation", "status.mute_conversation": "靜音對話",
"status.open": "展開文章", "status.open": "展開文章",
"status.pin": "Pin on profile", "status.pin": "置頂到資料頁",
"status.reblog": "轉推", "status.reblog": "轉推",
"status.reblogged_by": "{name} 轉推", "status.reblogged_by": "{name} 轉推",
"status.reply": "回應", "status.reply": "回應",
@ -182,8 +184,8 @@
"status.share": "Share", "status.share": "Share",
"status.show_less": "減少顯示", "status.show_less": "減少顯示",
"status.show_more": "顯示更多", "status.show_more": "顯示更多",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "解禁對話",
"status.unpin": "Unpin from profile", "status.unpin": "解除置頂",
"tabs_bar.compose": "撰寫", "tabs_bar.compose": "撰寫",
"tabs_bar.federated_timeline": "跨站", "tabs_bar.federated_timeline": "跨站",
"tabs_bar.home": "主頁", "tabs_bar.home": "主頁",
@ -193,6 +195,15 @@
"upload_button.label": "上載媒體檔案", "upload_button.label": "上載媒體檔案",
"upload_form.undo": "還原", "upload_form.undo": "還原",
"upload_progress.label": "上載中……", "upload_progress.label": "上載中……",
"video.close": "關閉影片",
"video.exit_fullscreen": "退出全熒幕",
"video.expand": "展開影片",
"video.fullscreen": "全熒幕",
"video.hide": "隱藏影片",
"video.mute": "靜音",
"video.pause": "暫停",
"video.play": "播放",
"video.unmute": "解除靜音",
"video_player.expand": "展開影片", "video_player.expand": "展開影片",
"video_player.toggle_sound": "開關音效", "video_player.toggle_sound": "開關音效",
"video_player.toggle_visible": "打開或關上", "video_player.toggle_visible": "打開或關上",

@ -1,11 +1,11 @@
{ {
"account.block": "封鎖 @{name}", "account.block": "封鎖 @{name}",
"account.block_domain": "隱藏來自 {domain} 的一切", "account.block_domain": "隱藏來自 {domain} 的一切貼文",
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.", "account.disclaimer_full": "下列資料不一定完整。",
"account.edit_profile": "編輯用資訊", "account.edit_profile": "編輯用資訊",
"account.follow": "關注", "account.follow": "關注",
"account.followers": "專注者", "account.followers": "專注者",
"account.follows": "正關注", "account.follows": "正關注",
"account.follows_you": "關注你", "account.follows_you": "關注你",
"account.media": "媒體", "account.media": "媒體",
"account.mention": "提到 @{name}", "account.mention": "提到 @{name}",
@ -13,19 +13,19 @@
"account.posts": "貼文", "account.posts": "貼文",
"account.report": "檢舉 @{name}", "account.report": "檢舉 @{name}",
"account.requested": "正在等待許可", "account.requested": "正在等待許可",
"account.share": "Share @{name}'s profile", "account.share": "分享 @{name} 的用者資訊",
"account.unblock": "取消封鎖 @{name}", "account.unblock": "取消封鎖 @{name}",
"account.unblock_domain": "不再隱藏 {domain}", "account.unblock_domain": "不再隱藏 {domain}",
"account.unfollow": "取消關注", "account.unfollow": "取消關注",
"account.unmute": "不再消音 @{name}", "account.unmute": "不再消音 @{name}",
"account.view_full_profile": "View full profile", "account.view_full_profile": "查看完整資訊",
"boost_modal.combo": "下次你可以按 {combo} 來跳過", "boost_modal.combo": "下次你可以按 {combo} 來跳過",
"bundle_column_error.body": "Something went wrong while loading this component.", "bundle_column_error.body": "加載本組件出錯。",
"bundle_column_error.retry": "Try again", "bundle_column_error.retry": "重試",
"bundle_column_error.title": "Network error", "bundle_column_error.title": "網路錯誤",
"bundle_modal_error.close": "Close", "bundle_modal_error.close": "關閉",
"bundle_modal_error.message": "Something went wrong while loading this component.", "bundle_modal_error.message": "加載本組件出錯。",
"bundle_modal_error.retry": "Try again", "bundle_modal_error.retry": "重試",
"column.blocks": "封鎖的使用者", "column.blocks": "封鎖的使用者",
"column.community": "本地時間軸", "column.community": "本地時間軸",
"column.favourites": "最愛", "column.favourites": "最愛",
@ -33,21 +33,22 @@
"column.home": "家", "column.home": "家",
"column.mutes": "消音的使用者", "column.mutes": "消音的使用者",
"column.notifications": "通知", "column.notifications": "通知",
"column.pins": "置頂貼文",
"column.public": "聯盟時間軸", "column.public": "聯盟時間軸",
"column_back_button.label": "上一頁", "column_back_button.label": "上一頁",
"column_header.hide_settings": "Hide settings", "column_header.hide_settings": "隱藏設定",
"column_header.moveLeft_settings": "Move column to the left", "column_header.moveLeft_settings": "將欄左移",
"column_header.moveRight_settings": "Move column to the right", "column_header.moveRight_settings": "將欄右移",
"column_header.pin": "Pin", "column_header.pin": "置頂",
"column_header.show_settings": "Show settings", "column_header.show_settings": "顯示設定",
"column_header.unpin": "Unpin", "column_header.unpin": "撤頂",
"column_subheading.navigation": "瀏覽", "column_subheading.navigation": "瀏覽",
"column_subheading.settings": "設定", "column_subheading.settings": "設定",
"compose_form.lock_disclaimer": "你的帳號沒有{locked}。任何人都可以關注你,看到發給關注者的貼文。", "compose_form.lock_disclaimer": "你的帳號沒有{locked}。任何人都可以關注你,看到發給關注者的貼文。",
"compose_form.lock_disclaimer.lock": "上鎖", "compose_form.lock_disclaimer.lock": "上鎖",
"compose_form.placeholder": "在想些什麼?", "compose_form.placeholder": "在想些什麼?",
"compose_form.privacy_disclaimer": "你的貼文會被傳到 {domains} 上被提到的使用者。你信任 {domainsCount, plural, one {這個伺服器} other {這些伺服器}}嗎?貼文的隱私設定只會在 Mastodon 副本上生效。如果 {domains} {domainsCount, plural, one {不是一個 Mastodon 副本} other {都不是 Mastodon 副本}},就不會被標記為非公開貼文,而且可能會被轉推或是讓不預期的人看見。", "compose_form.privacy_disclaimer": "你的貼文會被傳到 {domains} 上被提到的使用者。你信任 {domainsCount, plural, one {這個伺服器} other {這些伺服器}}嗎?貼文的隱私設定只會在 Mastodon 副本上生效。如果 {domains} {domainsCount, plural, one {不是一個 Mastodon 副本} other {都不是 Mastodon 副本}},就不會被標記為非公開貼文,而且可能會被轉推或是讓不預期的人看見。",
"compose_form.publish": "", "compose_form.publish": "貼掉",
"compose_form.publish_loud": "{publish}", "compose_form.publish_loud": "{publish}",
"compose_form.sensitive": "將此媒體標為敏感", "compose_form.sensitive": "將此媒體標為敏感",
"compose_form.spoiler": "將訊息隱藏在警告訊息之後", "compose_form.spoiler": "將訊息隱藏在警告訊息之後",
@ -58,13 +59,13 @@
"confirmations.delete.confirm": "刪除", "confirmations.delete.confirm": "刪除",
"confirmations.delete.message": "你確定要刪除這個狀態?", "confirmations.delete.message": "你確定要刪除這個狀態?",
"confirmations.domain_block.confirm": "隱藏整個網域", "confirmations.domain_block.confirm": "隱藏整個網域",
"confirmations.domain_block.message": "你真的真的確定要封鎖整個 {domain} ?多數情況下,比較推薦封鎖或消音幾個特定目標就好。", "confirmations.domain_block.message": "你真的真的確定要隱藏整個 {domain} ?多數情況下,比較推薦封鎖或消音幾個特定目標就好。",
"confirmations.mute.confirm": "消音", "confirmations.mute.confirm": "消音",
"confirmations.mute.message": "你確定要消音 {name} ", "confirmations.mute.message": "你確定要消音 {name} ",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "取消關注",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.message": "真的不要繼續關注 {name} 了嗎?",
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "要內嵌此貼文,請將以下代碼貼進你的網站。",
"embed.preview": "Here is what it will look like:", "embed.preview": "看上去會變成這樣:",
"emoji_button.activity": "活動", "emoji_button.activity": "活動",
"emoji_button.flags": "旗幟", "emoji_button.flags": "旗幟",
"emoji_button.food": "食物與飲料", "emoji_button.food": "食物與飲料",
@ -72,12 +73,12 @@
"emoji_button.nature": "自然", "emoji_button.nature": "自然",
"emoji_button.objects": "物件", "emoji_button.objects": "物件",
"emoji_button.people": "人", "emoji_button.people": "人",
"emoji_button.search": "搜尋...", "emoji_button.search": "搜尋",
"emoji_button.symbols": "符號", "emoji_button.symbols": "符號",
"emoji_button.travel": "旅遊與地點", "emoji_button.travel": "旅遊與地點",
"empty_column.community": "本地時間軸是空的。公開寫點什麼吧!", "empty_column.community": "本地時間軸是空的。公開寫點什麼吧!",
"empty_column.hashtag": "這個主題標籤下什麼都沒有。", "empty_column.hashtag": "這個主題標籤下什麼都沒有。",
"empty_column.home": "你還沒關注任何人。造訪{public}或利用搜尋功能找到其他用。", "empty_column.home": "你還沒關注任何人。造訪{public}或利用搜尋功能找到其他用。",
"empty_column.home.inactivity": "你家的訊息摘要是空的。如果你很久沒活動了,很快它就會重新產生。", "empty_column.home.inactivity": "你家的訊息摘要是空的。如果你很久沒活動了,很快它就會重新產生。",
"empty_column.home.public_timeline": "公開時間軸", "empty_column.home.public_timeline": "公開時間軸",
"empty_column.notifications": "還沒有任何通知。和別的使用者互動來開始對話。", "empty_column.notifications": "還沒有任何通知。和別的使用者互動來開始對話。",
@ -96,22 +97,23 @@
"home.column_settings.show_replies": "顯示回應", "home.column_settings.show_replies": "顯示回應",
"home.settings": "欄位設定", "home.settings": "欄位設定",
"lightbox.close": "關閉", "lightbox.close": "關閉",
"lightbox.next": "Next", "lightbox.next": "繼續",
"lightbox.previous": "Previous", "lightbox.previous": "回退",
"loading_indicator.label": "讀取中...", "loading_indicator.label": "讀取中...",
"media_gallery.toggle_visible": "切換可見性", "media_gallery.toggle_visible": "切換可見性",
"missing_indicator.label": "找不到", "missing_indicator.label": "找不到",
"navigation_bar.blocks": "封鎖的使用者", "navigation_bar.blocks": "封鎖的使用者",
"navigation_bar.community_timeline": "本地時間軸", "navigation_bar.community_timeline": "本地時間軸",
"navigation_bar.edit_profile": "編輯用資訊", "navigation_bar.edit_profile": "編輯用資訊",
"navigation_bar.favourites": "最愛", "navigation_bar.favourites": "最愛",
"navigation_bar.follow_requests": "關注請求", "navigation_bar.follow_requests": "關注請求",
"navigation_bar.info": "關於本站", "navigation_bar.info": "關於本站",
"navigation_bar.logout": "登出", "navigation_bar.logout": "登出",
"navigation_bar.mutes": "消音的使用者", "navigation_bar.mutes": "消音的使用者",
"navigation_bar.pins": "置頂貼文",
"navigation_bar.preferences": "偏好設定", "navigation_bar.preferences": "偏好設定",
"navigation_bar.public_timeline": "聯盟時間軸", "navigation_bar.public_timeline": "聯盟時間軸",
"notification.favourite": "{name}喜歡你的狀態", "notification.favourite": "{name}收藏了你的狀態",
"notification.follow": "{name}關注了你", "notification.follow": "{name}關注了你",
"notification.mention": "{name}提到了你", "notification.mention": "{name}提到了你",
"notification.reblog": "{name}推了你的狀態", "notification.reblog": "{name}推了你的狀態",
@ -121,8 +123,8 @@
"notifications.column_settings.favourite": "最愛:", "notifications.column_settings.favourite": "最愛:",
"notifications.column_settings.follow": "新的關注者:", "notifications.column_settings.follow": "新的關注者:",
"notifications.column_settings.mention": "提到:", "notifications.column_settings.mention": "提到:",
"notifications.column_settings.push": "Push notifications", "notifications.column_settings.push": "推送通知",
"notifications.column_settings.push_meta": "This device", "notifications.column_settings.push_meta": "這臺設備",
"notifications.column_settings.reblog": "轉推:", "notifications.column_settings.reblog": "轉推:",
"notifications.column_settings.show": "顯示在欄位中", "notifications.column_settings.show": "顯示在欄位中",
"notifications.column_settings.sound": "播放音效", "notifications.column_settings.sound": "播放音效",
@ -135,8 +137,8 @@
"onboarding.page_one.handle": "你在 {domain} 上,所以你的帳號全名是 {handle}", "onboarding.page_one.handle": "你在 {domain} 上,所以你的帳號全名是 {handle}",
"onboarding.page_one.welcome": "歡迎來到 Mastodon ", "onboarding.page_one.welcome": "歡迎來到 Mastodon ",
"onboarding.page_six.admin": "你的副本的管理員是 {admin} 。", "onboarding.page_six.admin": "你的副本的管理員是 {admin} 。",
"onboarding.page_six.almost_done": "快好了...", "onboarding.page_six.almost_done": "快好了",
"onboarding.page_six.appetoot": "口大開!", "onboarding.page_six.appetoot": "口大開!",
"onboarding.page_six.apps_available": "在 iOS 、 Android 和其他平台上有這些 {apps} 可以用。", "onboarding.page_six.apps_available": "在 iOS 、 Android 和其他平台上有這些 {apps} 可以用。",
"onboarding.page_six.github": "Mastodon 是自由的開源軟體。你可以在 {github} 上回報臭蟲、請求新功能或是做出貢獻。", "onboarding.page_six.github": "Mastodon 是自由的開源軟體。你可以在 {github} 上回報臭蟲、請求新功能或是做出貢獻。",
"onboarding.page_six.guidelines": "社群指南", "onboarding.page_six.guidelines": "社群指南",
@ -161,17 +163,17 @@
"report.target": "通報中", "report.target": "通報中",
"search.placeholder": "搜尋", "search.placeholder": "搜尋",
"search_results.total": "{count, number} 項結果", "search_results.total": "{count, number} 項結果",
"standalone.public_title": "A look inside...", "standalone.public_title": "站點一瞥…",
"status.cannot_reblog": "此貼文無法轉推", "status.cannot_reblog": "此貼文無法轉推",
"status.delete": "刪除", "status.delete": "刪除",
"status.embed": "Embed", "status.embed": "Embed",
"status.favourite": "喜愛", "status.favourite": "收藏",
"status.load_more": "載入更多", "status.load_more": "載入更多",
"status.media_hidden": "媒體已隱藏", "status.media_hidden": "媒體已隱藏",
"status.mention": "提到 @{name}", "status.mention": "提到 @{name}",
"status.mute_conversation": "消音對話", "status.mute_conversation": "消音對話",
"status.open": "展開這個狀態", "status.open": "展開這個狀態",
"status.pin": "Pin on profile", "status.pin": "置頂到個人資訊頁",
"status.reblog": "轉推", "status.reblog": "轉推",
"status.reblogged_by": "{name} 轉推了", "status.reblogged_by": "{name} 轉推了",
"status.reply": "回應", "status.reply": "回應",
@ -183,7 +185,7 @@
"status.show_less": "看少點", "status.show_less": "看少點",
"status.show_more": "看更多", "status.show_more": "看更多",
"status.unmute_conversation": "不消音對話", "status.unmute_conversation": "不消音對話",
"status.unpin": "Unpin from profile", "status.unpin": "解除置頂",
"tabs_bar.compose": "編輯", "tabs_bar.compose": "編輯",
"tabs_bar.federated_timeline": "聯盟", "tabs_bar.federated_timeline": "聯盟",
"tabs_bar.home": "家", "tabs_bar.home": "家",
@ -193,6 +195,15 @@
"upload_button.label": "增加媒體", "upload_button.label": "增加媒體",
"upload_form.undo": "復原", "upload_form.undo": "復原",
"upload_progress.label": "上傳中...", "upload_progress.label": "上傳中...",
"video.close": "關閉影片",
"video.exit_fullscreen": "退出全熒幕",
"video.expand": "展開影片",
"video.fullscreen": "全熒幕",
"video.hide": "隱藏影片",
"video.mute": "消音",
"video.pause": "暫停",
"video.play": "播放",
"video.unmute": "解除消音",
"video_player.expand": "展開影片", "video_player.expand": "展開影片",
"video_player.toggle_sound": "切換音效", "video_player.toggle_sound": "切換音效",
"video_player.toggle_visible": "切換可見性", "video_player.toggle_visible": "切換可見性",

@ -0,0 +1,23 @@
import { Map as ImmutableMap } from 'immutable';
import { HEIGHT_CACHE_SET, HEIGHT_CACHE_CLEAR } from '../actions/height_cache';
const initialState = ImmutableMap();
const setHeight = (state, key, id, height) => {
return state.update(key, ImmutableMap(), map => map.set(id, height));
};
const clearHeights = () => {
return ImmutableMap();
};
export default function statuses(state = initialState, action) {
switch(action.type) {
case HEIGHT_CACHE_SET:
return setHeight(state, action.key, action.id, action.height);
case HEIGHT_CACHE_CLEAR:
return clearHeights();
default:
return state;
}
};

@ -21,6 +21,7 @@ import compose from './compose';
import search from './search'; import search from './search';
import media_attachments from './media_attachments'; import media_attachments from './media_attachments';
import notifications from './notifications'; import notifications from './notifications';
import height_cache from './height_cache';
const reducers = { const reducers = {
timelines, timelines,
@ -45,6 +46,7 @@ const reducers = {
search, search,
media_attachments, media_attachments,
notifications, notifications,
height_cache,
}; };
export default combineReducers(reducers); export default combineReducers(reducers);

@ -15,8 +15,6 @@ import {
CONTEXT_FETCH_SUCCESS, CONTEXT_FETCH_SUCCESS,
STATUS_MUTE_SUCCESS, STATUS_MUTE_SUCCESS,
STATUS_UNMUTE_SUCCESS, STATUS_UNMUTE_SUCCESS,
STATUS_SET_HEIGHT,
STATUSES_CLEAR_HEIGHT,
} from '../actions/statuses'; } from '../actions/statuses';
import { import {
TIMELINE_REFRESH_SUCCESS, TIMELINE_REFRESH_SUCCESS,
@ -60,9 +58,14 @@ const normalizeStatus = (state, status) => {
} }
const searchContent = [status.spoiler_text, status.content].join(' ').replace(/<br \/>/g, '\n').replace(/<\/p><p>/g, '\n\n'); const searchContent = [status.spoiler_text, status.content].join(' ').replace(/<br \/>/g, '\n').replace(/<\/p><p>/g, '\n\n');
const emojiMap = normalStatus.emojis.reduce((obj, emoji) => {
obj[`:${emoji.shortcode}:`] = emoji.url;
return obj;
}, {});
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
normalStatus.contentHtml = emojify(normalStatus.content); normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(normalStatus.spoiler_text || '')); normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(normalStatus.spoiler_text || ''), emojiMap);
return state.update(status.id, ImmutableMap(), map => map.mergeDeep(fromJS(normalStatus))); return state.update(status.id, ImmutableMap(), map => map.mergeDeep(fromJS(normalStatus)));
}; };
@ -95,18 +98,6 @@ const filterStatuses = (state, relationship) => {
return state; return state;
}; };
const setHeight = (state, id, height) => {
return state.update(id, ImmutableMap(), map => map.set('height', height));
};
const clearHeights = (state) => {
state.forEach(status => {
state = state.deleteIn([status.get('id'), 'height']);
});
return state;
};
const initialState = ImmutableMap(); const initialState = ImmutableMap();
export default function statuses(state = initialState, action) { export default function statuses(state = initialState, action) {
@ -148,10 +139,6 @@ export default function statuses(state = initialState, action) {
return deleteStatus(state, action.id, action.references); return deleteStatus(state, action.id, action.references);
case ACCOUNT_BLOCK_SUCCESS: case ACCOUNT_BLOCK_SUCCESS:
return filterStatuses(state, action.relationship); return filterStatuses(state, action.relationship);
case STATUS_SET_HEIGHT:
return setHeight(state, action.id, action.height);
case STATUSES_CLEAR_HEIGHT:
return clearHeights(state);
default: default:
return state; return state;
} }

@ -25,6 +25,11 @@ function main() {
const emojify = require('../mastodon/emoji').default; const emojify = require('../mastodon/emoji').default;
const { getLocale } = require('../mastodon/locales'); const { getLocale } = require('../mastodon/locales');
const { localeData } = getLocale(); const { localeData } = getLocale();
const VideoContainer = require('../mastodon/containers/video_container').default;
const MediaGalleryContainer = require('../mastodon/containers/media_gallery_container').default;
const CardContainer = require('../mastodon/containers/card_container').default;
const React = require('react');
const ReactDOM = require('react-dom');
localeData.forEach(IntlRelativeFormat.__addLocaleData); localeData.forEach(IntlRelativeFormat.__addLocaleData);
@ -66,22 +71,21 @@ function main() {
window.open(e.target.href, 'mastodon-intent', 'width=400,height=400,resizable=no,menubar=no,status=no,scrollbars=yes'); window.open(e.target.href, 'mastodon-intent', 'width=400,height=400,resizable=no,menubar=no,status=no,scrollbars=yes');
}); });
}); });
});
delegate(document, '.video-player video', 'click', ({ target }) => { [].forEach.call(document.querySelectorAll('[data-component="Video"]'), (content) => {
if (target.paused) { const props = JSON.parse(content.getAttribute('data-props'));
target.play(); ReactDOM.render(<VideoContainer locale={locale} {...props} />, content);
} else { });
target.pause();
}
});
delegate(document, '.activity-stream .media-spoiler-wrapper .media-spoiler', 'click', function() { [].forEach.call(document.querySelectorAll('[data-component="MediaGallery"]'), (content) => {
this.parentNode.classList.add('media-spoiler-wrapper__visible'); const props = JSON.parse(content.getAttribute('data-props'));
}); ReactDOM.render(<MediaGalleryContainer locale={locale} {...props} />, content);
});
delegate(document, '.activity-stream .media-spoiler-wrapper .spoiler-button', 'click', function() { [].forEach.call(document.querySelectorAll('[data-component="Card"]'), (content) => {
this.parentNode.classList.remove('media-spoiler-wrapper__visible'); const props = JSON.parse(content.getAttribute('data-props'));
ReactDOM.render(<CardContainer locale={locale} {...props} />, content);
});
}); });
delegate(document, '.webapp-btn', 'click', ({ target, button }) => { delegate(document, '.webapp-btn', 'click', ({ target, button }) => {
@ -126,7 +130,7 @@ function main() {
delegate(document, '#account_avatar', 'change', ({ target }) => { delegate(document, '#account_avatar', 'change', ({ target }) => {
const avatar = document.querySelector('.card.compact .avatar img'); const avatar = document.querySelector('.card.compact .avatar img');
const [file] = target.files || []; const [file] = target.files || [];
const url = URL.createObjectURL(file); const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
avatar.src = url; avatar.src = url;
}); });
@ -134,7 +138,7 @@ function main() {
delegate(document, '#account_header', 'change', ({ target }) => { delegate(document, '#account_header', 'change', ({ target }) => {
const header = document.querySelector('.card.compact'); const header = document.querySelector('.card.compact');
const [file] = target.files || []; const [file] = target.files || [];
const url = URL.createObjectURL(file); const url = file ? URL.createObjectURL(file) : header.dataset.originalSrc;
header.style.backgroundImage = `url(${url})`; header.style.backgroundImage = `url(${url})`;
}); });

@ -137,7 +137,7 @@
padding-bottom: 15px; padding-bottom: 15px;
.hero .heading { .hero .heading {
padding-bottom: 30px; padding-bottom: 20px;
font-family: 'mastodon-font-sans-serif', sans-serif; font-family: 'mastodon-font-sans-serif', sans-serif;
font-size: 16px; font-size: 16px;
font-weight: 400; font-weight: 400;
@ -327,7 +327,7 @@
.about-short { .about-short {
background: darken($ui-base-color, 4%); background: darken($ui-base-color, 4%);
padding: 50px 0; padding: 50px 0 30px;
font-family: 'mastodon-font-sans-serif', sans-serif; font-family: 'mastodon-font-sans-serif', sans-serif;
font-size: 16px; font-size: 16px;
font-weight: 400; font-weight: 400;
@ -640,8 +640,11 @@
.header-wrapper { .header-wrapper {
padding-top: 0; padding-top: 0;
&.compact {
padding-bottom: 0;
}
&.compact .hero .heading { &.compact .hero .heading {
padding-bottom: 20px;
text-align: initial; text-align: initial;
} }
} }

@ -97,6 +97,14 @@
margin-bottom: 40px; margin-bottom: 40px;
} }
h3 {
color: $ui-secondary-color;
font-size: 20px;
line-height: 28px;
font-weight: 400;
margin-bottom: 30px;
}
h6 { h6 {
font-size: 16px; font-size: 16px;
color: $ui-secondary-color; color: $ui-secondary-color;

@ -631,6 +631,10 @@
opacity: 1; opacity: 1;
animation: fade 150ms linear; animation: fade 150ms linear;
.video-player {
margin-top: 8px;
}
&.status-direct { &.status-direct {
background: lighten($ui-base-color, 8%); background: lighten($ui-base-color, 8%);
@ -867,6 +871,10 @@
height: 22px; height: 22px;
} }
} }
.video-player {
margin-top: 8px;
}
} }
.detailed-status__meta { .detailed-status__meta {
@ -1610,9 +1618,8 @@
.column, .column,
.drawer { .drawer {
@supports(display: grid) { // hack to fix Chrome <57 flex: 1 1 100%;
contain: strict; overflow: hidden;
}
} }
@include limited-single-column('screen and (max-width: 360px)', $parent: null) { @include limited-single-column('screen and (max-width: 360px)', $parent: null) {
@ -1790,9 +1797,7 @@
overflow-x: hidden; overflow-x: hidden;
flex: 1 1 auto; flex: 1 1 auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
@supports(display: grid) { // hack to fix Chrome <57 will-change: transform; // improves perf in mobile Chrome
contain: strict;
}
&.optionally-scrollable { &.optionally-scrollable {
overflow-y: auto; overflow-y: auto;
@ -2642,7 +2647,7 @@ button.icon-button.active i.fa-retweet {
.media-spoiler { .media-spoiler {
background: $base-overlay-background; background: $base-overlay-background;
color: $primary-text-color; color: $ui-primary-color;
border: 0; border: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -4206,6 +4211,182 @@ button.icon-button.active i.fa-retweet {
z-index: 5; z-index: 5;
} }
.video-player {
overflow: hidden;
position: relative;
background: $base-shadow-color;
max-width: 100%;
video {
height: 100%;
width: 100%;
z-index: 1;
}
&.fullscreen {
width: 100% !important;
height: 100% !important;
margin: 0;
video {
max-width: 100% !important;
max-height: 100% !important;
}
}
&.inline {
video {
object-fit: cover;
position: relative;
top: 50%;
transform: translateY(-50%);
}
}
&__controls {
position: absolute;
z-index: 2;
bottom: 0;
left: 0;
right: 0;
box-sizing: border-box;
background: linear-gradient(0deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 60%, transparent);
padding: 0 10px;
opacity: 0;
transition: opacity .1s ease;
&.active {
opacity: 1;
}
}
&.inactive {
video,
.video-player__controls {
visibility: hidden;
}
}
&__spoiler {
display: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 4;
border: 0;
background: $base-shadow-color;
color: $ui-primary-color;
transition: none;
pointer-events: none;
&.active {
display: block;
pointer-events: auto;
&:hover,
&:active,
&:focus {
color: lighten($ui-primary-color, 8%);
}
}
&__title {
display: block;
font-size: 14px;
}
&__subtitle {
display: block;
font-size: 11px;
font-weight: 500;
}
}
&__buttons {
padding-bottom: 10px;
font-size: 16px;
&.left {
float: left;
button {
padding-right: 10px;
}
}
&.right {
float: right;
button {
padding-left: 10px;
}
}
button {
background: transparent;
padding: 0;
border: 0;
color: $white;
&:active,
&:hover,
&:focus {
color: $ui-highlight-color;
}
}
}
&__seek {
cursor: pointer;
height: 24px;
position: relative;
&::before {
content: "";
width: 100%;
background: rgba($white, 0.35);
display: block;
position: absolute;
height: 4px;
top: 10px;
}
&__progress {
display: block;
position: absolute;
height: 4px;
top: 10px;
background: $ui-highlight-color;
}
&__handle {
position: absolute;
z-index: 3;
opacity: 0;
border-radius: 50%;
width: 12px;
height: 12px;
top: 6px;
margin-left: -6px;
transition: opacity .1s ease;
background: $ui-highlight-color;
pointer-events: none;
&.active {
opacity: 1;
}
}
&:hover {
.video-player__seek__handle {
opacity: 1;
}
}
}
}
.media-spoiler-video { .media-spoiler-video {
background-size: cover; background-size: cover;
background-repeat: no-repeat; background-repeat: no-repeat;

@ -349,9 +349,46 @@ code {
box-shadow: 0 0 5px rgba($base-shadow-color, 0.2); box-shadow: 0 0 5px rgba($base-shadow-color, 0.2);
text-align: center; text-align: center;
p {
margin-bottom: 15px;
}
.oauth-code {
color: $ui-secondary-color;
outline: 0;
box-sizing: border-box;
display: block;
width: 100%;
border: none;
padding: 10px;
font-family: 'mastodon-font-monospace', monospace;
background: $ui-base-color;
color: $ui-primary-color;
font-size: 14px;
margin: 0;
&::-moz-focus-inner {
border: 0;
}
&::-moz-focus-inner,
&:focus,
&:active {
outline: 0 !important;
}
&:focus {
background: lighten($ui-base-color, 4%);
}
}
strong { strong {
font-weight: 500; font-weight: 500;
} }
@media screen and (max-width: 740px) and (min-width: 441px) {
margin-top: 40px;
}
} }
.form-footer { .form-footer {

@ -140,19 +140,6 @@
} }
} }
} }
.status__attachments {
margin-top: 8px;
overflow: hidden;
width: 100%;
box-sizing: border-box;
position: relative;
.status__attachments__inner {
display: flex;
height: 214px;
}
}
} }
.detailed-status.light { .detailed-status.light {
@ -233,139 +220,35 @@
} }
} }
.detailed-status__attachments { .status-card {
margin-top: 8px; border-color: lighten($ui-secondary-color, 4%);
overflow: hidden; color: darken($ui-primary-color, 4%);
width: 100%;
box-sizing: border-box;
position: relative;
.status__attachments__inner { &:hover {
display: flex; background: lighten($ui-secondary-color, 4%);
height: 360px;
} }
} }
.video-player { .status-card__title,
margin-top: 8px; .status-card__description {
height: 300px; color: $ui-base-color;
overflow: hidden;
position: relative;
video {
position: relative;
z-index: 1;
width: 100%;
height: 100%;
object-fit: cover;
top: 50%;
transform: translateY(-50%);
}
}
}
.media-item,
.video-item {
box-sizing: border-box;
position: relative;
left: auto;
top: auto;
right: auto;
bottom: auto;
float: left;
border: medium none;
display: block;
flex: 1 1 auto;
width: 100%;
height: 100%;
overflow: hidden;
margin-right: 2px;
&:last-child {
margin-right: 0;
}
a {
display: block;
width: 100%;
height: 100%;
background: no-repeat scroll center center / cover;
text-decoration: none;
cursor: zoom-in;
}
video {
position: relative;
z-index: 1;
width: 100%;
height: 100%;
object-fit: cover;
top: 50%;
transform: translateY(-50%);
}
}
.video-item {
a {
cursor: pointer;
} }
.video-item__play { .status-card__image {
position: absolute; background: $ui-secondary-color;
top: 50%;
left: 50%;
font-size: 36px;
transform: translate(-50%, -50%);
padding: 5px;
border-radius: 100px;
color: rgba($primary-text-color, 0.8);
z-index: 1;
} }
} }
.media-spoiler { .media-spoiler {
background: $ui-primary-color; background: $ui-primary-color;
width: 100%; color: $white;
height: 100%;
cursor: pointer;
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
text-align: center;
transition: all 100ms linear; transition: all 100ms linear;
z-index: 2;
&:hover { &:hover,
&:active,
&:focus {
background: darken($ui-primary-color, 5%); background: darken($ui-primary-color, 5%);
} color: unset;
span {
display: block;
&:first-child {
font-size: 14px;
}
&:last-child {
font-size: 11px;
font-weight: 500;
}
}
}
.media-spoiler-wrapper {
&.media-spoiler-wrapper__visible {
.media-spoiler {
display: none;
}
.spoiler-button {
display: block;
}
} }
} }

@ -11,7 +11,12 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
return status unless status.nil? return status unless status.nil?
status = Status.create!(account: @account, reblog: original_status, uri: @json['id']) status = Status.create!(
account: @account,
reblog: original_status,
uri: @json['id'],
created_at: @json['published'] || Time.now.utc
)
distribute(status) distribute(status)
status status
end end

@ -4,26 +4,31 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
def perform def perform
return if delete_arrived_first?(object_uri) || unsupported_object_type? return if delete_arrived_first?(object_uri) || unsupported_object_type?
status = find_existing_status RedisLock.acquire(lock_options) do |lock|
if lock.acquired?
@status = find_existing_status
process_status if @status.nil?
end
end
@status
end
return status unless status.nil? private
def process_status
ApplicationRecord.transaction do ApplicationRecord.transaction do
status = Status.create!(status_params) @status = Status.create!(status_params)
process_tags(status) process_tags(@status)
process_attachments(status) process_attachments(@status)
end end
resolve_thread(status) resolve_thread(@status)
distribute(status) distribute(@status)
forward_for_reply if status.public_visibility? || status.unlisted_visibility? forward_for_reply if @status.public_visibility? || @status.unlisted_visibility?
status
end end
private
def find_existing_status def find_existing_status
status = status_from_uri(object_uri) status = status_from_uri(object_uri)
status ||= Status.find_by(uri: @object['atomUri']) if @object['atomUri'].present? status ||= Status.find_by(uri: @object['atomUri']) if @object['atomUri'].present?
@ -56,6 +61,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
process_hashtag tag, status process_hashtag tag, status
when 'Mention' when 'Mention'
process_mention tag, status process_mention tag, status
when 'Emoji'
process_emoji tag, status
end end
end end
end end
@ -74,6 +81,17 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
account.mentions.create(status: status) account.mentions.create(status: status)
end end
def process_emoji(tag, _status)
shortcode = tag['name'].delete(':')
emoji = CustomEmoji.find_by(shortcode: shortcode, domain: @account.domain)
return if !emoji.nil? || skip_download?
emoji = CustomEmoji.new(domain: @account.domain, shortcode: shortcode)
emoji.image_remote_url = tag['href']
emoji.save
end
def process_attachments(status) def process_attachments(status)
return unless @object['attachment'].is_a?(Array) return unless @object['attachment'].is_a?(Array)
@ -182,4 +200,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
return unless @json['signature'].present? && reply_to_local? return unless @json['signature'].present? && reply_to_local?
ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id) ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id)
end end
def lock_options
{ redis: Redis.current, key: "create:#{@object['id']}" }
end
end end

@ -14,6 +14,8 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
'atomUri' => 'ostatus:atomUri', 'atomUri' => 'ostatus:atomUri',
'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri', 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri',
'conversation' => 'ostatus:conversation', 'conversation' => 'ostatus:conversation',
'toot' => 'http://joinmastodon.org/ns#',
'Emoji' => 'toot:Emoji',
}, },
], ],
}.freeze }.freeze
@ -28,7 +30,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
def serializable_hash(options = nil) def serializable_hash(options = nil)
options = serialization_options(options) options = serialization_options(options)
serialized_hash = CONTEXT.merge(ActiveModelSerializers::Adapter::Attributes.new(serializer, instance_options).serializable_hash(options)) serialized_hash = ActiveModelSerializers::Adapter::Attributes.new(serializer, instance_options).serializable_hash(options)
self.class.transform_key_casing!(serialized_hash, instance_options) CONTEXT.merge(self.class.transform_key_casing!(serialized_hash, instance_options))
end end
end end

@ -37,7 +37,7 @@ class ActivityPub::TagManager
end end
def activity_uri_for(target) def activity_uri_for(target)
return nil unless %i(note comment activity).include?(target.object_type) && target.local? raise ArgumentError, 'target must be a local activity' unless %i(note comment activity).include?(target.object_type) && target.local?
activity_account_status_url(target.account, target) activity_account_status_url(target.account, target)
end end

@ -9,7 +9,7 @@ class Formatter
include ActionView::Helpers::TextHelper include ActionView::Helpers::TextHelper
def format(status) def format(status, options = {})
if status.reblog? if status.reblog?
prepend_reblog = status.reblog.account.acct prepend_reblog = status.reblog.account.acct
status = status.proper status = status.proper
@ -19,7 +19,11 @@ class Formatter
raw_content = status.text raw_content = status.text
return reformat(raw_content) unless status.local? unless status.local?
html = reformat(raw_content)
html = encode_custom_emojis(html, status.emojis) if options[:custom_emojify]
return html
end
linkable_accounts = status.mentions.map(&:account) linkable_accounts = status.mentions.map(&:account)
linkable_accounts << status.account linkable_accounts << status.account
@ -27,6 +31,7 @@ class Formatter
html = raw_content html = raw_content
html = "RT @#{prepend_reblog} #{html}" if prepend_reblog html = "RT @#{prepend_reblog} #{html}" if prepend_reblog
html = encode_and_link_urls(html, linkable_accounts) html = encode_and_link_urls(html, linkable_accounts)
html = encode_custom_emojis(html, status.emojis) if options[:custom_emojify]
html = simple_format(html, {}, sanitize: false) html = simple_format(html, {}, sanitize: false)
html = html.delete("\n") html = html.delete("\n")
@ -39,7 +44,9 @@ class Formatter
def plaintext(status) def plaintext(status)
return status.text if status.local? return status.text if status.local?
strip_tags(status.text)
text = status.text.gsub(/(<br \/>|<br>|<\/p>)+/) { |match| "#{match}\n" }
strip_tags(text)
end end
def simplified_format(account) def simplified_format(account)
@ -76,6 +83,47 @@ class Formatter
end end
end end
def encode_custom_emojis(html, emojis)
return html if emojis.empty?
emoji_map = emojis.map { |e| [e.shortcode, full_asset_url(e.image.url)] }.to_h
i = -1
inside_tag = false
inside_shortname = false
shortname_start_index = -1
while i + 1 < html.size
i += 1
if inside_shortname && html[i] == ':'
shortcode = html[shortname_start_index + 1..i - 1]
emoji = emoji_map[shortcode]
if emoji
replacement = "<img draggable=\"false\" class=\"emojione\" alt=\":#{shortcode}:\" title=\":#{shortcode}:\" src=\"#{emoji}\" />"
before_html = shortname_start_index.positive? ? html[0..shortname_start_index - 1] : ''
html = before_html + replacement + html[i + 1..-1]
i += replacement.size - (shortcode.size + 2) - 1
else
i -= 1
end
inside_shortname = false
elsif inside_tag && html[i] == '>'
inside_tag = false
elsif html[i] == '<'
inside_tag = true
inside_shortname = false
elsif !inside_tag && html[i] == ':'
inside_shortname = true
shortname_start_index = i
end
end
html
end
def rewrite(text, entities) def rewrite(text, entities)
chars = text.to_s.to_char_a chars = text.to_s.to_char_a
@ -131,13 +179,13 @@ class Formatter
end end
def link_html(url) def link_html(url)
url = Addressable::URI.parse(url).display_uri.to_s url = Addressable::URI.parse(url).to_s
prefix = url.match(/\Ahttps?:\/\/(www\.)?/).to_s prefix = url.match(/\Ahttps?:\/\/(www\.)?/).to_s
text = url[prefix.length, 30] text = url[prefix.length, 30]
suffix = url[prefix.length + 30..-1] suffix = url[prefix.length + 30..-1]
cutoff = url[prefix.length..-1].length > 30 cutoff = url[prefix.length..-1].length > 30
"<span class=\"invisible\">#{prefix}</span><span class=\"#{cutoff ? 'ellipsis' : ''}\">#{text}</span><span class=\"invisible\">#{suffix}</span>" "<span class=\"invisible\">#{encode(prefix)}</span><span class=\"#{cutoff ? 'ellipsis' : ''}\">#{encode(text)}</span><span class=\"invisible\">#{encode(suffix)}</span>"
end end
def hashtag_html(tag) def hashtag_html(tag)

@ -1,26 +1,31 @@
# frozen_string_literal: true # frozen_string_literal: true
class LanguageDetector class LanguageDetector
attr_reader :text, :account include Singleton
def initialize(text, account = nil) def initialize
@text = text
@account = account
@identifier = CLD3::NNetLanguageIdentifier.new(1, 2048) @identifier = CLD3::NNetLanguageIdentifier.new(1, 2048)
end end
def to_iso_s def detect(text, account)
detected_language_code || default_locale detect_language_code(text) || default_locale(account)
end end
def prepared_text def language_names
simplified_text.strip @language_names =
CLD3::TaskContextParams::LANGUAGE_NAMES.map { |name| iso6391(name.to_s).to_sym }
.uniq
end end
private private
def detected_language_code def prepare_text(text)
iso6391(result.language).to_sym if detected_language_reliable? simplify_text(text).strip
end
def detect_language_code(text)
result = @identifier.find_language(prepare_text(text))
iso6391(result.language.to_s).to_sym if result.reliable?
end end
def iso6391(bcp47) def iso6391(bcp47)
@ -32,15 +37,7 @@ class LanguageDetector
ISO_639.find(iso639).alpha2 ISO_639.find(iso639).alpha2
end end
def result def simplify_text(text)
@result ||= @identifier.find_language(prepared_text)
end
def detected_language_reliable?
result.reliable?
end
def simplified_text
text.dup.tap do |new_text| text.dup.tap do |new_text|
new_text.gsub!(FetchLinkCardService::URL_PATTERN, '') new_text.gsub!(FetchLinkCardService::URL_PATTERN, '')
new_text.gsub!(Account::MENTION_RE, '') new_text.gsub!(Account::MENTION_RE, '')
@ -49,7 +46,7 @@ class LanguageDetector
end end
end end
def default_locale def default_locale(account)
account&.user_locale&.to_sym || nil account.user_locale&.to_sym
end end
end end

@ -42,6 +42,7 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
save_mentions(status) save_mentions(status)
save_hashtags(status) save_hashtags(status)
save_media(status) save_media(status)
save_emojis(status)
end end
if thread? && status.thread.nil? if thread? && status.thread.nil?
@ -150,6 +151,25 @@ class OStatus::Activity::Creation < OStatus::Activity::Base
end end
end end
def save_emojis(parent)
do_not_download = DomainBlock.find_by(domain: parent.account.domain)&.reject_media?
return if do_not_download
@xml.xpath('./xmlns:link[@rel="emoji"]', xmlns: TagManager::XMLNS).each do |link|
next unless link['href'] && link['name']
shortcode = link['name'].delete(':')
emoji = CustomEmoji.find_by(shortcode: shortcode, domain: parent.account.domain)
next unless emoji.nil?
emoji = CustomEmoji.new(shortcode: shortcode, domain: parent.account.domain)
emoji.image_remote_url = link['href']
emoji.save
end
end
def account_from_href(href) def account_from_href(href)
url = Addressable::URI.parse(href).normalize url = Addressable::URI.parse(href).normalize

@ -368,5 +368,9 @@ class OStatus::AtomSerializer
end end
append_element(entry, 'mastodon:scope', status.visibility) append_element(entry, 'mastodon:scope', status.visibility)
status.emojis.each do |emoji|
append_element(entry, 'link', nil, rel: :emoji, href: full_asset_url(emoji.image.url), name: emoji.shortcode)
end
end end
end end

@ -31,6 +31,8 @@ class Request
def perform def perform
http_client.headers(headers).public_send(@verb, @url.to_s, @options) http_client.headers(headers).public_send(@verb, @url.to_s, @options)
rescue => e
raise e.class, "#{e.message} on #{@url}"
end end
def headers def headers

@ -87,7 +87,7 @@ class TagManager
def local_url?(url) def local_url?(url)
uri = Addressable::URI.parse(url).normalize uri = Addressable::URI.parse(url).normalize
domain = uri.host + (uri.port ? ":#{uri.port}" : '') domain = uri.host + (uri.port ? ":#{uri.port}" : '')
TagManager.instance.local_domain?(domain) TagManager.instance.web_domain?(domain)
end end
def uri_for(target) def uri_for(target)

@ -106,6 +106,7 @@ class Account < ApplicationRecord
scope :by_domain_accounts, -> { group(:domain).select(:domain, 'COUNT(*) AS accounts_count').order('accounts_count desc') } scope :by_domain_accounts, -> { group(:domain).select(:domain, 'COUNT(*) AS accounts_count').order('accounts_count desc') }
scope :matches_username, ->(value) { where(arel_table[:username].matches("#{value}%")) } scope :matches_username, ->(value) { where(arel_table[:username].matches("#{value}%")) }
scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) } scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) }
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
delegate :email, delegate :email,
:current_sign_in_ip, :current_sign_in_ip,
@ -174,6 +175,10 @@ class Account < ApplicationRecord
end end
class << self class << self
def readonly_attributes
super - %w(statuses_count following_count followers_count)
end
def domains def domains
reorder(nil).pluck('distinct accounts.domain') reorder(nil).pluck('distinct accounts.domain')
end end

@ -27,9 +27,11 @@ module Remotable
matches = response.headers['content-disposition']&.match(/filename="([^"]*)"/) matches = response.headers['content-disposition']&.match(/filename="([^"]*)"/)
filename = matches.nil? ? parsed_url.path.split('/').last : matches[1] filename = matches.nil? ? parsed_url.path.split('/').last : matches[1]
basename = SecureRandom.hex(8)
extname = File.extname(filename)
send("#{attachment_name}=", StringIO.new(response.to_s)) send("#{attachment_name}=", StringIO.new(response.to_s))
send("#{attachment_name}_file_name=", filename) send("#{attachment_name}_file_name=", basename + extname)
self[attribute_name] = url if has_attribute?(attribute_name) self[attribute_name] = url if has_attribute?(attribute_name)
rescue HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, Paperclip::Errors::NotIdentifiedByImageMagickError, Addressable::URI::InvalidURIError => e rescue HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, Paperclip::Errors::NotIdentifiedByImageMagickError, Addressable::URI::InvalidURIError => e

@ -0,0 +1,38 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: custom_emojis
#
# id :integer not null, primary key
# shortcode :string default(""), not null
# domain :string
# image_file_name :string
# image_content_type :string
# image_file_size :integer
# image_updated_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
#
class CustomEmoji < ApplicationRecord
SHORTCODE_RE_FRAGMENT = '[a-zA-Z0-9_]{2,}'
SCAN_RE = /(?<=[^[:alnum:]:]|\n|^)
:(#{SHORTCODE_RE_FRAGMENT}):
(?=[^[:alnum:]:]|$)/x
has_attached_file :image
validates_attachment :image, content_type: { content_type: 'image/png' }, presence: true, size: { in: 0..50.kilobytes }
validates :shortcode, uniqueness: { scope: :domain }, format: { with: /\A#{SHORTCODE_RE_FRAGMENT}\z/ }, length: { minimum: 2 }
include Remotable
class << self
def from_text(text, domain)
return [] if text.blank?
shortcodes = text.scan(SCAN_RE).map(&:first)
where(shortcode: shortcodes, domain: domain)
end
end
end

@ -0,0 +1,28 @@
# frozen_string_literal: true
class InstanceFilter
attr_reader :params
def initialize(params)
@params = params
end
def results
scope = Account.remote.by_domain_accounts
params.each do |key, value|
scope.merge!(scope_for(key, value)) if value.present?
end
scope
end
private
def scope_for(key, value)
case key.to_s
when 'domain_name'
Account.matches_domain(value)
else
raise "Unknown filter: #{key}"
end
end
end

@ -56,15 +56,21 @@ class MediaAttachment < ApplicationRecord
validates :account, presence: true validates :account, presence: true
scope :attached, -> { where.not(status_id: nil) } scope :attached, -> { where.not(status_id: nil) }
scope :unattached, -> { where(status_id: nil) } scope :unattached, -> { where(status_id: nil) }
scope :local, -> { where(remote_url: '') } scope :local, -> { where(remote_url: '') }
scope :remote, -> { where.not(remote_url: '') }
default_scope { order(id: :asc) } default_scope { order(id: :asc) }
def local? def local?
remote_url.blank? remote_url.blank?
end end
def needs_redownload?
file.blank? && remote_url.present?
end
def to_param def to_param
shortcode shortcode
end end

@ -0,0 +1,44 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: site_uploads
#
# id :integer not null, primary key
# var :string default(""), not null
# file_file_name :string
# file_content_type :string
# file_file_size :integer
# file_updated_at :datetime
# meta :json
# created_at :datetime not null
# updated_at :datetime not null
#
class SiteUpload < ApplicationRecord
has_attached_file :file
validates_attachment_content_type :file, content_type: /\Aimage\/.*\z/
validates :var, presence: true, uniqueness: true
before_save :set_meta
after_commit :clear_cache
def cache_key
"site_uploads/#{var}"
end
private
def set_meta
tempfile = file.queued_for_write[:original]
return if tempfile.nil?
geometry = Paperclip::Geometry.from_file(tempfile)
self.meta = { width: geometry.width.to_i, height: geometry.height.to_i }
end
def clear_cache
Rails.cache.delete(cache_key)
end
end

@ -55,7 +55,7 @@ class Status < ApplicationRecord
has_one :notification, as: :activity, dependent: :destroy has_one :notification, as: :activity, dependent: :destroy
has_one :stream_entry, as: :activity, inverse_of: :status has_one :stream_entry, as: :activity, inverse_of: :status
validates :uri, uniqueness: true, unless: :local? validates :uri, uniqueness: true, presence: true, unless: :local?
validates :text, presence: true, unless: :reblog? validates :text, presence: true, unless: :reblog?
validates_with StatusLengthValidator validates_with StatusLengthValidator
validates :reblog, uniqueness: { scope: :account }, if: :reblog? validates :reblog, uniqueness: { scope: :account }, if: :reblog?
@ -70,7 +70,6 @@ class Status < ApplicationRecord
scope :without_reblogs, -> { where('statuses.reblog_of_id IS NULL') } scope :without_reblogs, -> { where('statuses.reblog_of_id IS NULL') }
scope :with_public_visibility, -> { where(visibility: :public) } scope :with_public_visibility, -> { where(visibility: :public) }
scope :tagged_with, ->(tag) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag }) } scope :tagged_with, ->(tag) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag }) }
scope :local_only, -> { left_outer_joins(:account).where(accounts: { domain: nil }) }
scope :excluding_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced: false }) } scope :excluding_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced: false }) }
scope :including_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced: true }) } scope :including_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced: true }) }
scope :not_excluded_by_account, ->(account) { where.not(account_id: account.excluded_from_timeline_account_ids) } scope :not_excluded_by_account, ->(account) { where.not(account_id: account.excluded_from_timeline_account_ids) }
@ -132,6 +131,10 @@ class Status < ApplicationRecord
!sensitive? && media_attachments.any? !sensitive? && media_attachments.any?
end end
def emojis
CustomEmoji.from_text(text, account.domain)
end
after_create :store_uri, if: :local? after_create :store_uri, if: :local?
before_validation :prepare_contents, if: :local? before_validation :prepare_contents, if: :local?
@ -221,7 +224,7 @@ class Status < ApplicationRecord
private private
def timeline_scope(local_only = false) def timeline_scope(local_only = false)
starting_scope = local_only ? Status.local_only : Status starting_scope = local_only ? Status.local : Status
starting_scope starting_scope
.with_public_visibility .with_public_visibility
.without_reblogs .without_reblogs

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

Loading…
Cancel
Save