This commit is contained in:
Baptiste Lemoine 2020-07-09 10:49:02 +02:00
commit ef0f131f1b
509 changed files with 14609 additions and 5400 deletions

View File

@ -1,28 +0,0 @@
version: 1
update_configs:
- package_manager: "ruby:bundler"
directory: "/"
update_schedule: "weekly"
# Supported update schedule: live daily weekly monthly
version_requirement_updates: "auto"
# Supported version requirements: auto widen_ranges increase_versions increase_versions_if_necessary
allowed_updates:
- match:
dependency_type: "all"
# Supported dependency types: all indirect direct production development
update_type: "all"
# Supported update types: all security
- package_manager: "javascript"
directory: "/"
update_schedule: "weekly"
# Supported update schedule: live daily weekly monthly
version_requirement_updates: "auto"
# Supported version requirements: auto widen_ranges increase_versions increase_versions_if_necessary
allowed_updates:
- match:
dependency_type: "all"
# Supported dependency types: all indirect direct production development
update_type: "all"
# Supported update types: all security

View File

@ -1,262 +1,60 @@
# Service dependencies # This is a sample configuration file. You can generate your configuration
# You may set REDIS_URL instead for more advanced options # with the `rake mastodon:setup` interactive setup wizard, but to customize
# You may also set REDIS_NAMESPACE to share Redis between multiple Mastodon servers # your setup even further, you'll need to edit it manually. This sample does
REDIS_HOST=redis # not demonstrate all available configuration options. Please look at
REDIS_PORT=6379 # https://docs.joinmastodon/admin/config/ for the full documentation.
# You may set DATABASE_URL instead for more advanced options
DB_HOST=db
DB_USER=postgres
DB_NAME=postgres
DB_PASS=
DB_PORT=5432
# Optional ElasticSearch configuration
# You may also set ES_PREFIX to share the same cluster between multiple Mastodon servers (falls back to REDIS_NAMESPACE if not set)
# ES_ENABLED=true
# ES_HOST=es
# ES_PORT=9200
# Federation # Federation
# Note: Changing LOCAL_DOMAIN at a later time will cause unwanted side effects, including breaking all existing federation. # ----------
# LOCAL_DOMAIN should *NOT* contain the protocol part of the domain e.g https://example.com. # This identifies your server and cannot be changed safely later
# ----------
LOCAL_DOMAIN=example.com LOCAL_DOMAIN=example.com
# Changing LOCAL_HTTPS in production is no longer supported. (Mastodon will always serve https:// links) # Redis
# -----
REDIS_HOST=localhost
REDIS_PORT=6379
# Use this only if you need to run mastodon on a different domain than the one used for federation. # PostgreSQL
# You can read more about this option on https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Serving_a_different_domain.md # ----------
# DO *NOT* USE THIS UNLESS YOU KNOW *EXACTLY* WHAT YOU ARE DOING. DB_HOST=/var/run/postgresql
# WEB_DOMAIN=mastodon.example.com DB_USER=mastodon
DB_NAME=mastodon_production
DB_PASS=
DB_PORT=5432
# Use this if you want to have several aliases handler@example1.com # ElasticSearch (optional)
# handler@example2.com etc. for the same user. LOCAL_DOMAIN should not # ------------------------
# be added. Comma separated values ES_ENABLED=true
# ALTERNATE_DOMAINS=example1.com,example2.com ES_HOST=localhost
ES_PORT=9200
# Application secrets # Secrets
# Generate each with the `RAILS_ENV=production bundle exec rake secret` task (`docker-compose run --rm web bundle exec rake secret` if you use docker compose) # -------
# Make sure to use `rake secret` to generate secrets
# -------
SECRET_KEY_BASE= SECRET_KEY_BASE=
OTP_SECRET= OTP_SECRET=
# VAPID keys (used for push notifications # Web Push
# You can generate the keys using the following command (first is the private key, second is the public one) # --------
# You should only generate this once per instance. If you later decide to change it, all push subscription will # Generate with `rake mastodon:webpush:generate_vapid_key`
# be invalidated, requiring the users to access the website again to resubscribe. # --------
#
# Generate with `RAILS_ENV=production bundle exec rake mastodon:webpush:generate_vapid_key` task (`docker-compose run --rm web bundle exec rake mastodon:webpush:generate_vapid_key` if you use docker compose)
#
# For more information visit https://rossta.net/blog/using-the-web-push-api-with-vapid.html
VAPID_PRIVATE_KEY= VAPID_PRIVATE_KEY=
VAPID_PUBLIC_KEY= VAPID_PUBLIC_KEY=
# Registrations # Sending mail
# Single user mode will disable registrations and redirect frontpage to the first profile # ------------
# SINGLE_USER_MODE=true
# Prevent registrations with following e-mail domains
# EMAIL_DOMAIN_BLACKLIST=example1.com|example2.de|etc
# Only allow registrations with the following e-mail domains
# EMAIL_DOMAIN_WHITELIST=example1.com|example2.de|etc
# Optionally change default language
# DEFAULT_LOCALE=de
# E-mail configuration
# Note: Mailgun and SparkPost (https://sparkpo.st/smtp) each have good free tiers
# If you want to use an SMTP server without authentication (e.g local Postfix relay)
# then set SMTP_AUTH_METHOD and SMTP_OPENSSL_VERIFY_MODE to 'none' and
# *comment* SMTP_LOGIN and SMTP_PASSWORD (leaving them blank is not enough).
SMTP_SERVER=smtp.mailgun.org SMTP_SERVER=smtp.mailgun.org
SMTP_PORT=587 SMTP_PORT=587
SMTP_LOGIN= SMTP_LOGIN=
SMTP_PASSWORD= SMTP_PASSWORD=
SMTP_FROM_ADDRESS=notifications@example.com SMTP_FROM_ADDRESS=notificatons@example.com
#SMTP_REPLY_TO=
#SMTP_DOMAIN= # defaults to LOCAL_DOMAIN
#SMTP_DELIVERY_METHOD=smtp # delivery method can also be sendmail
#SMTP_AUTH_METHOD=plain
#SMTP_CA_FILE=/etc/ssl/certs/ca-certificates.crt
#SMTP_OPENSSL_VERIFY_MODE=peer
#SMTP_ENABLE_STARTTLS_AUTO=true
#SMTP_TLS=true
# Optional user upload path and URL (images, avatars). Default is :rails_root/public/system. If you set this variable, you are responsible for making your HTTP server (eg. nginx) serve these files. # File storage (optional)
# PAPERCLIP_ROOT_PATH=/var/lib/mastodon/public-system # -----------------------
# PAPERCLIP_ROOT_URL=/system S3_ENABLED=true
S3_BUCKET=files.example.com
# Optional asset host for multi-server setups AWS_ACCESS_KEY_ID=
# The asset host must allow cross origin request from WEB_DOMAIN or LOCAL_DOMAIN AWS_SECRET_ACCESS_KEY=
# if WEB_DOMAIN is not set. For example, the server may have the S3_ALIAS_HOST=files.example.com
# following header field:
# Access-Control-Allow-Origin: https://example.com/
# CDN_HOST=https://assets.example.com
# S3 (optional)
# The attachment host must allow cross origin request from WEB_DOMAIN or
# LOCAL_DOMAIN if WEB_DOMAIN is not set. For example, the server may have the
# following header field:
# Access-Control-Allow-Origin: https://192.168.1.123:9000/
# S3_ENABLED=true
# S3_BUCKET=
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
# S3_REGION=
# S3_PROTOCOL=http
# S3_HOSTNAME=192.168.1.123:9000
# S3 (Minio Config (optional) Please check Minio instance for details)
# The attachment host must allow cross origin request - see the description
# above.
# S3_ENABLED=true
# S3_BUCKET=
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
# S3_REGION=
# S3_PROTOCOL=https
# S3_HOSTNAME=
# S3_ENDPOINT=
# S3_SIGNATURE_VERSION=
# Google Cloud Storage (optional)
# Use S3 compatible API. Since GCS does not support Multipart Upload,
# increase the value of S3_MULTIPART_THRESHOLD to disable Multipart Upload.
# The attachment host must allow cross origin request - see the description
# above.
# S3_ENABLED=true
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
# S3_REGION=
# S3_PROTOCOL=https
# S3_HOSTNAME=storage.googleapis.com
# S3_ENDPOINT=https://storage.googleapis.com
# S3_MULTIPART_THRESHOLD=52428801 # 50.megabytes
# Swift (optional)
# The attachment host must allow cross origin request - see the description
# above.
# SWIFT_ENABLED=true
# SWIFT_USERNAME=
# For Keystone V3, the value for SWIFT_TENANT should be the project name
# SWIFT_TENANT=
# SWIFT_PASSWORD=
# Some OpenStack V3 providers require PROJECT_ID (optional)
# SWIFT_PROJECT_ID=
# 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_CONTAINER=
# 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 (e.g. to serve files on a custom domain, possibly using Cloudfront or Cloudflare)
# S3_ALIAS_HOST=
# Streaming API integration
# STREAMING_API_BASE_URL=
# Advanced settings
# If you need to use pgBouncer, you need to disable prepared statements:
# PREPARED_STATEMENTS=false
# Cluster number setting for streaming API server.
# If you comment out following line, cluster number will be `numOfCpuCores - 1`.
STREAMING_CLUSTER_NUM=1
# Docker mastodon user
# If you use Docker, you may want to assign UID/GID manually.
# UID=1000
# GID=1000
# LDAP authentication (optional)
# LDAP_ENABLED=true
# LDAP_HOST=localhost
# LDAP_PORT=389
# LDAP_METHOD=simple_tls
# LDAP_BASE=
# LDAP_BIND_DN=
# LDAP_PASSWORD=
# LDAP_UID=cn
# LDAP_MAIL=mail
# LDAP_SEARCH_FILTER=(|(%{uid}=%{email})(%{mail}=%{email}))
# LDAP_UID_CONVERSION_ENABLED=true
# LDAP_UID_CONVERSION_SEARCH=., -
# LDAP_UID_CONVERSION_REPLACE=_
# PAM authentication (optional)
# PAM authentication uses for the email generation the "email" pam variable
# and optional as fallback PAM_DEFAULT_SUFFIX
# The pam environment variable "email" is provided by:
# https://github.com/devkral/pam_email_extractor
# PAM_ENABLED=true
# Fallback email domain for email address generation (LOCAL_DOMAIN by default)
# PAM_EMAIL_DOMAIN=example.com
# Name of the pam service (pam "auth" section is evaluated)
# PAM_DEFAULT_SERVICE=rpam
# Name of the pam service used for checking if an user can register (pam "account" section is evaluated) (nil (disabled) by default)
# PAM_CONTROLLED_SERVICE=rpam
# Global OAuth settings (optional) :
# If you have only one strategy, you may want to enable this
# OAUTH_REDIRECT_AT_SIGN_IN=true
# Optional CAS authentication (cf. omniauth-cas) :
# CAS_ENABLED=true
# CAS_URL=https://sso.myserver.com/
# CAS_HOST=sso.myserver.com/
# CAS_PORT=443
# CAS_SSL=true
# CAS_VALIDATE_URL=
# CAS_CALLBACK_URL=
# CAS_LOGOUT_URL=
# CAS_LOGIN_URL=
# CAS_UID_FIELD='user'
# CAS_CA_PATH=
# CAS_DISABLE_SSL_VERIFICATION=false
# CAS_UID_KEY='user'
# CAS_NAME_KEY='name'
# CAS_EMAIL_KEY='email'
# CAS_NICKNAME_KEY='nickname'
# CAS_FIRST_NAME_KEY='firstname'
# CAS_LAST_NAME_KEY='lastname'
# CAS_LOCATION_KEY='location'
# CAS_IMAGE_KEY='image'
# CAS_PHONE_KEY='phone'
# Optional SAML authentication (cf. omniauth-saml)
# SAML_ENABLED=true
# SAML_ACS_URL=http://localhost:3000/auth/auth/saml/callback
# SAML_ISSUER=https://example.com
# SAML_IDP_SSO_TARGET_URL=https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO
# SAML_IDP_CERT=
# SAML_IDP_CERT_FINGERPRINT=
# SAML_NAME_IDENTIFIER_FORMAT=
# SAML_CERT=
# SAML_PRIVATE_KEY=
# SAML_SECURITY_WANT_ASSERTION_SIGNED=true
# SAML_SECURITY_WANT_ASSERTION_ENCRYPTED=true
# SAML_SECURITY_ASSUME_EMAIL_IS_VERIFIED=true
# SAML_ATTRIBUTES_STATEMENTS_UID="urn:oid:0.9.2342.19200300.100.1.1"
# SAML_ATTRIBUTES_STATEMENTS_EMAIL="urn:oid:1.3.6.1.4.1.5923.1.1.1.6"
# SAML_ATTRIBUTES_STATEMENTS_FULL_NAME="urn:oid:2.16.840.1.113730.3.1.241"
# SAML_ATTRIBUTES_STATEMENTS_FIRST_NAME="urn:oid:2.5.4.42"
# SAML_ATTRIBUTES_STATEMENTS_LAST_NAME="urn:oid:2.5.4.4"
# SAML_UID_ATTRIBUTE="urn:oid:0.9.2342.19200300.100.1.1"
# SAML_ATTRIBUTES_STATEMENTS_VERIFIED=
# SAML_ATTRIBUTES_STATEMENTS_VERIFIED_EMAIL=
# Use HTTP proxy for outgoing request (optional)
# http_proxy=http://gateway.local:8118
# Access control for hidden service.
# ALLOW_ACCESS_TO_HIDDEN_SERVICE=true
# Authorized fetch mode (optional)
# Require remote servers to authentify when fetching toots, see
# https://docs.joinmastodon.org/admin/config/#authorized_fetch
# AUTHORIZED_FETCH=true
# Whitelist mode (optional)
# Only allow federation with whitelisted domains, see
# https://docs.joinmastodon.org/admin/config/#whitelist_mode
# WHITELIST_MODE=true

View File

@ -1,3 +1,4 @@
VAGRANT=true VAGRANT=true
LOCAL_DOMAIN=mastodon.local LOCAL_DOMAIN=mastodon.local
BIND=0.0.0.0 BIND=0.0.0.0
DB_HOST=/var/run/postgresql/

1
.github/FUNDING.yml vendored
View File

@ -1,2 +1,3 @@
patreon: mastodon patreon: mastodon
open_collective: mastodon open_collective: mastodon
github: [Gargron]

22
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,22 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 99
allow:
- dependency-type: all
- package-ecosystem: bundler
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 99
allow:
- dependency-type: all

27
.gitignore vendored
View File

@ -17,31 +17,36 @@
/log/* /log/*
!/log/.keep !/log/.keep
/tmp /tmp
coverage /coverage
public/system /public/system
public/assets /public/assets
public/packs /public/packs
public/packs-test /public/packs-test
.env .env
.env.production .env.production
.env.development .env.development
node_modules/ /node_modules/
build/ /build/
# Ignore Vagrant files # Ignore Vagrant files
.vagrant/ .vagrant/
# Ignore Capistrano customizations # Ignore Capistrano customizations
config/deploy/* /config/deploy/*
# Ignore IDE files # Ignore IDE files
.vscode/ .vscode/
.idea/ .idea/
# Ignore postgres + redis + elasticsearch volume optionally created by docker-compose # Ignore postgres + redis + elasticsearch volume optionally created by docker-compose
postgres /postgres
redis /redis
elasticsearch /elasticsearch
# ignore Helm lockfile, dependency charts, and local values file
/chart/Chart.lock
/chart/charts/*.tgz
/chart/values.yaml
# Ignore Apple files # Ignore Apple files
.DS_Store .DS_Store

View File

@ -1,11 +1,11 @@
FROM ubuntu:18.04 as build-dep FROM ubuntu:20.04 as build-dep
# Use bash for the shell # Use bash for the shell
SHELL ["bash", "-c"] SHELL ["bash", "-c"]
# Install Node v12 (LTS) # Install Node v12 (LTS)
ENV NODE_VER="12.16.1" ENV NODE_VER="12.16.3"
RUN ARCH= && \ RUN ARCH= && \
dpkgArch="$(dpkg --print-architecture)" && \ dpkgArch="$(dpkg --print-architecture)" && \
case "${dpkgArch##*-}" in \ case "${dpkgArch##*-}" in \
amd64) ARCH='x64';; \ amd64) ARCH='x64';; \
@ -74,7 +74,7 @@ RUN cd /opt/mastodon && \
bundle install -j$(nproc) && \ bundle install -j$(nproc) && \
yarn install --pure-lockfile yarn install --pure-lockfile
FROM ubuntu:18.04 FROM ubuntu:20.04
# Copy over all the langs needed for runtime # Copy over all the langs needed for runtime
COPY --from=build-dep /opt/node /opt/node COPY --from=build-dep /opt/node /opt/node
@ -98,8 +98,8 @@ RUN apt update && \
# Install mastodon runtime deps # Install mastodon runtime deps
RUN apt -y --no-install-recommends install \ RUN apt -y --no-install-recommends install \
libssl1.1 libpq5 imagemagick ffmpeg \ libssl1.1 libpq5 imagemagick ffmpeg \
libicu60 libprotobuf10 libidn11 libyaml-0-2 \ libicu66 libprotobuf17 libidn11 libyaml-0-2 \
file ca-certificates tzdata libreadline7 && \ file ca-certificates tzdata libreadline8 && \
apt -y install gcc && \ apt -y install gcc && \
ln -s /opt/mastodon /mastodon && \ ln -s /opt/mastodon /mastodon && \
gem install bundler && \ gem install bundler && \

25
Gemfile
View File

@ -9,7 +9,7 @@ gem 'puma', '~> 4.3'
gem 'rails', '~> 5.2.4.3' gem 'rails', '~> 5.2.4.3'
gem 'sprockets', '~> 3.7.2' gem 'sprockets', '~> 3.7.2'
gem 'thor', '~> 0.20' gem 'thor', '~> 0.20'
gem 'rack', '~> 2.2.2' gem 'rack', '~> 2.2.3'
gem 'thwait', '~> 0.1.0' gem 'thwait', '~> 0.1.0'
gem 'e2mmap', '~> 0.1.0' gem 'e2mmap', '~> 0.1.0'
@ -20,7 +20,7 @@ gem 'makara', '~> 0.4'
gem 'pghero', '~> 2.5' gem 'pghero', '~> 2.5'
gem 'dotenv-rails', '~> 2.7' gem 'dotenv-rails', '~> 2.7'
gem 'aws-sdk-s3', '~> 1.66', require: false gem 'aws-sdk-s3', '~> 1.73', require: false
gem 'fog-core', '<= 2.1.0' gem 'fog-core', '<= 2.1.0'
gem 'fog-openstack', '~> 0.3', require: false gem 'fog-openstack', '~> 0.3', require: false
gem 'paperclip', '~> 6.0' gem 'paperclip', '~> 6.0'
@ -48,6 +48,7 @@ gem 'omniauth-cas', '~> 1.1'
gem 'omniauth-saml', '~> 1.10' gem 'omniauth-saml', '~> 1.10'
gem 'omniauth', '~> 1.9' gem 'omniauth', '~> 1.9'
gem 'color_diff', '~> 0.1'
gem 'discard', '~> 1.2' gem 'discard', '~> 1.2'
gem 'doorkeeper', '~> 5.4' gem 'doorkeeper', '~> 5.4'
gem 'ed25519', '~> 1.2' gem 'ed25519', '~> 1.2'
@ -61,7 +62,7 @@ gem 'htmlentities', '~> 4.3'
gem 'http', '~> 4.4' gem 'http', '~> 4.4'
gem 'http_accept_language', '~> 2.1' gem 'http_accept_language', '~> 2.1'
gem 'http_parser.rb', '~> 0.6', git: 'https://github.com/tmm1/http_parser.rb', ref: '54b17ba8c7d8d20a16dfc65d1775241833219cf2', submodules: true gem 'http_parser.rb', '~> 0.6', git: 'https://github.com/tmm1/http_parser.rb', ref: '54b17ba8c7d8d20a16dfc65d1775241833219cf2', submodules: true
gem 'httplog', '~> 1.4.2' gem 'httplog', '~> 1.4.3'
gem 'idn-ruby', require: 'idn' gem 'idn-ruby', require: 'idn'
gem 'kaminari', '~> 1.2' gem 'kaminari', '~> 1.2'
gem 'link_header', '~> 0.0' gem 'link_header', '~> 0.0'
@ -80,11 +81,11 @@ gem 'rack-attack', '~> 6.3'
gem 'rack-cors', '~> 1.1', require: 'rack/cors' gem 'rack-cors', '~> 1.1', require: 'rack/cors'
gem 'rails-i18n', '~> 5.1' gem 'rails-i18n', '~> 5.1'
gem 'rails-settings-cached', '~> 0.6' gem 'rails-settings-cached', '~> 0.6'
gem 'redis', '~> 4.1', require: ['redis', 'redis/connection/hiredis'] gem 'redis', '~> 4.2', require: ['redis', 'redis/connection/hiredis']
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
gem 'rqrcode', '~> 1.1' gem 'rqrcode', '~> 1.1'
gem 'ruby-progressbar', '~> 1.10' gem 'ruby-progressbar', '~> 1.10'
gem 'sanitize', '~> 5.1' gem 'sanitize', '~> 5.2'
gem 'sidekiq', '~> 6.0' gem 'sidekiq', '~> 6.0'
gem 'sidekiq-scheduler', '~> 3.0' gem 'sidekiq-scheduler', '~> 3.0'
gem 'sidekiq-unique-jobs', '~> 6.0' gem 'sidekiq-unique-jobs', '~> 6.0'
@ -118,15 +119,15 @@ group :production, :test do
end end
group :test do group :test do
gem 'capybara', '~> 3.32' gem 'capybara', '~> 3.33'
gem 'climate_control', '~> 0.2' gem 'climate_control', '~> 0.2'
gem 'faker', '~> 2.12' gem 'faker', '~> 2.13'
gem 'microformats', '~> 4.2' gem 'microformats', '~> 4.2'
gem 'rails-controller-testing', '~> 1.0' gem 'rails-controller-testing', '~> 1.0'
gem 'rspec-sidekiq', '~> 3.0' gem 'rspec-sidekiq', '~> 3.1'
gem 'simplecov', '~> 0.18', require: false gem 'simplecov', '~> 0.18', require: false
gem 'webmock', '~> 3.8' gem 'webmock', '~> 3.8'
gem 'parallel_tests', '~> 2.32' gem 'parallel_tests', '~> 3.0'
gem 'rspec_junit_formatter', '~> 0.4' gem 'rspec_junit_formatter', '~> 0.4'
end end
@ -139,10 +140,10 @@ group :development do
gem 'letter_opener', '~> 1.7' gem 'letter_opener', '~> 1.7'
gem 'letter_opener_web', '~> 1.4' gem 'letter_opener_web', '~> 1.4'
gem 'memory_profiler' gem 'memory_profiler'
gem 'rubocop', '~> 0.84', require: false gem 'rubocop', '~> 0.86', require: false
gem 'rubocop-rails', '~> 2.5', require: false gem 'rubocop-rails', '~> 2.6', require: false
gem 'brakeman', '~> 4.8', require: false gem 'brakeman', '~> 4.8', require: false
gem 'bundler-audit', '~> 0.6', require: false gem 'bundler-audit', '~> 0.7', require: false
gem 'capistrano', '~> 3.14' gem 'capistrano', '~> 3.14'
gem 'capistrano-rails', '~> 1.5' gem 'capistrano-rails', '~> 1.5'

View File

@ -86,27 +86,27 @@ GEM
activerecord (>= 3.2, < 7.0) activerecord (>= 3.2, < 7.0)
rake (>= 10.4, < 14.0) rake (>= 10.4, < 14.0)
arel (9.0.0) arel (9.0.0)
ast (2.4.0) ast (2.4.1)
attr_encrypted (3.1.0) attr_encrypted (3.1.0)
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-eventstream (1.1.0) aws-eventstream (1.1.0)
aws-partitions (1.322.0) aws-partitions (1.338.0)
aws-sdk-core (3.96.1) aws-sdk-core (3.103.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0) aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
jmespath (~> 1.0) jmespath (~> 1.0)
aws-sdk-kms (1.31.0) aws-sdk-kms (1.36.0)
aws-sdk-core (~> 3, >= 3.71.0) aws-sdk-core (~> 3, >= 3.99.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.66.0) aws-sdk-s3 (1.73.0)
aws-sdk-core (~> 3, >= 3.96.1) aws-sdk-core (~> 3, >= 3.102.1)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sigv4 (1.1.4) aws-sigv4 (1.2.1)
aws-eventstream (~> 1.0, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
bcrypt (3.1.13) bcrypt (3.1.13)
better_errors (2.7.1) better_errors (2.7.1)
coderay (>= 1.0.0) coderay (>= 1.0.0)
@ -124,11 +124,11 @@ GEM
bullet (6.1.0) bullet (6.1.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.11) uniform_notifier (~> 1.11)
bundler-audit (0.6.1) bundler-audit (0.7.0.1)
bundler (>= 1.2.0, < 3) bundler (>= 1.2.0, < 3)
thor (~> 0.18) thor (>= 0.18, < 2)
byebug (11.1.3) byebug (11.1.3)
capistrano (3.14.0) capistrano (3.14.1)
airbrussh (>= 1.0.0) airbrussh (>= 1.0.0)
i18n i18n
rake (>= 10.0.0) rake (>= 10.0.0)
@ -143,7 +143,7 @@ GEM
sshkit (~> 1.3) sshkit (~> 1.3)
capistrano-yarn (2.0.2) capistrano-yarn (2.0.2)
capistrano (~> 3.0) capistrano (~> 3.0)
capybara (3.32.2) capybara (3.33.0)
addressable addressable
mini_mime (>= 0.1.3) mini_mime (>= 0.1.3)
nokogiri (~> 1.8) nokogiri (~> 1.8)
@ -165,15 +165,16 @@ GEM
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.3) coderay (1.1.3)
color_diff (0.1)
concurrent-ruby (1.1.6) concurrent-ruby (1.1.6)
connection_pool (2.2.2) connection_pool (2.2.3)
crack (0.4.3) crack (0.4.3)
safe_yaml (~> 1.0.0) safe_yaml (~> 1.0.0)
crass (1.0.6) crass (1.0.6)
css_parser (1.7.1) css_parser (1.7.1)
addressable addressable
debug_inspector (0.0.3) debug_inspector (0.0.3)
devise (4.7.1) devise (4.7.2)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
railties (>= 4.1.0) railties (>= 4.1.0)
@ -188,7 +189,7 @@ GEM
devise_pam_authenticatable2 (9.2.0) devise_pam_authenticatable2 (9.2.0)
devise (>= 4.0.0) devise (>= 4.0.0)
rpam2 (~> 4.0) rpam2 (~> 4.0)
diff-lcs (1.3) diff-lcs (1.4.4)
discard (1.2.0) discard (1.2.0)
activerecord (>= 4.2, < 7) activerecord (>= 4.2, < 7)
docile (1.3.2) docile (1.3.2)
@ -202,13 +203,13 @@ GEM
railties (>= 3.2, < 6.1) railties (>= 3.2, < 6.1)
e2mmap (0.1.0) e2mmap (0.1.0)
ed25519 (1.2.4) ed25519 (1.2.4)
elasticsearch (7.7.0) elasticsearch (7.8.0)
elasticsearch-api (= 7.7.0) elasticsearch-api (= 7.8.0)
elasticsearch-transport (= 7.7.0) elasticsearch-transport (= 7.8.0)
elasticsearch-api (7.7.0) elasticsearch-api (7.8.0)
multi_json multi_json
elasticsearch-dsl (0.1.9) elasticsearch-dsl (0.1.9)
elasticsearch-transport (7.7.0) elasticsearch-transport (7.8.0)
faraday (~> 1) faraday (~> 1)
multi_json multi_json
encryptor (3.0.0) encryptor (3.0.0)
@ -216,9 +217,9 @@ GEM
erubi (1.9.0) erubi (1.9.0)
et-orbi (1.2.4) et-orbi (1.2.4)
tzinfo tzinfo
excon (0.73.0) excon (0.75.0)
fabrication (2.21.1) fabrication (2.21.1)
faker (2.12.0) faker (2.13.0)
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
faraday (1.0.1) faraday (1.0.1)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
@ -236,7 +237,7 @@ GEM
fog-json (1.2.0) fog-json (1.2.0)
fog-core fog-core
multi_json (~> 1.10) multi_json (~> 1.10)
fog-openstack (0.3.7) fog-openstack (0.3.10)
fog-core (>= 1.45, <= 2.1.0) fog-core (>= 1.45, <= 2.1.0)
fog-json (>= 1.0) fog-json (>= 1.0)
ipaddress (>= 0.8) ipaddress (>= 0.8)
@ -282,10 +283,10 @@ GEM
http-parser (1.2.1) http-parser (1.2.1)
ffi-compiler (>= 1.0, < 2.0) ffi-compiler (>= 1.0, < 2.0)
http_accept_language (2.1.1) http_accept_language (2.1.1)
httplog (1.4.2) httplog (1.4.3)
rack (>= 1.0) rack (>= 1.0)
rainbow (>= 2.0.0) rainbow (>= 2.0.0)
i18n (1.8.2) i18n (1.8.3)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
i18n-tasks (0.9.31) i18n-tasks (0.9.31)
activesupport (>= 4.0.2) activesupport (>= 4.0.2)
@ -301,7 +302,7 @@ GEM
ipaddress (0.8.3) ipaddress (0.8.3)
iso-639 (0.3.5) iso-639 (0.3.5)
jmespath (1.4.0) jmespath (1.4.0)
json (2.3.0) json (2.3.1)
json-canonicalization (0.2.0) json-canonicalization (0.2.0)
json-ld (3.1.4) json-ld (3.1.4)
htmlentities (~> 4.3) htmlentities (~> 4.3)
@ -341,7 +342,7 @@ GEM
activesupport (>= 4) activesupport (>= 4)
railties (>= 4) railties (>= 4)
request_store (~> 1.0) request_store (~> 1.0)
loofah (2.5.0) loofah (2.6.0)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
mail (2.7.1) mail (2.7.1)
@ -371,7 +372,7 @@ GEM
net-ldap (0.16.2) net-ldap (0.16.2)
net-scp (3.0.0) net-scp (3.0.0)
net-ssh (>= 2.6.5, < 7.0.0) net-ssh (>= 2.6.5, < 7.0.0)
net-ssh (6.0.2) net-ssh (6.1.0)
nio4r (2.5.2) nio4r (2.5.2)
nokogiri (1.10.9) nokogiri (1.10.9)
mini_portile2 (~> 2.4.0) mini_portile2 (~> 2.4.0)
@ -390,9 +391,9 @@ GEM
addressable (~> 2.3) addressable (~> 2.3)
nokogiri (~> 1.5) nokogiri (~> 1.5)
omniauth (~> 1.2) omniauth (~> 1.2)
omniauth-saml (1.10.1) omniauth-saml (1.10.2)
omniauth (~> 1.3, >= 1.3.2) omniauth (~> 1.3, >= 1.3.2)
ruby-saml (~> 1.7) ruby-saml (~> 1.9)
orm_adapter (0.5.0) orm_adapter (0.5.0)
ox (2.13.2) ox (2.13.2)
paperclip (6.0.0) paperclip (6.0.0)
@ -404,17 +405,17 @@ 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.19.1) parallel (1.19.2)
parallel_tests (2.32.0) parallel_tests (3.0.0)
parallel parallel
parser (2.7.1.3) parser (2.7.1.4)
ast (~> 2.4.0) ast (~> 2.4.1)
parslet (2.0.0) parslet (2.0.0)
pastel (0.7.4) pastel (0.7.4)
equatable (~> 0.6) equatable (~> 0.6)
tty-color (~> 0.5) tty-color (~> 0.5)
pg (1.2.3) pg (1.2.3)
pghero (2.5.0) pghero (2.5.1)
activerecord (>= 5) activerecord (>= 5)
pkg-config (1.4.1) pkg-config (1.4.1)
premailer (1.11.1) premailer (1.11.1)
@ -439,13 +440,11 @@ GEM
pundit (2.1.0) pundit (2.1.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.3.1) raabro (1.3.1)
rack (2.2.2) rack (2.2.3)
rack-attack (6.3.1) rack-attack (6.3.1)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rack-cors (1.1.1) rack-cors (1.1.1)
rack (>= 2.0.0) rack (>= 2.0.0)
rack-protection (2.0.8.1)
rack
rack-proxy (0.6.5) rack-proxy (0.6.5)
rack rack
rack-test (1.1.0) rack-test (1.1.0)
@ -463,10 +462,10 @@ GEM
bundler (>= 1.3.0) bundler (>= 1.3.0)
railties (= 5.2.4.3) railties (= 5.2.4.3)
sprockets-rails (>= 2.0.0) sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.4) rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.x) actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.x) actionview (>= 5.0.1.rc1)
activesupport (>= 5.0.1.x) activesupport (>= 5.0.1.rc1)
rails-dom-testing (2.0.3) rails-dom-testing (2.0.3)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
nokogiri (>= 1.6) nokogiri (>= 1.6)
@ -485,12 +484,12 @@ GEM
thor (>= 0.19.0, < 2.0) thor (>= 0.19.0, < 2.0)
rainbow (3.0.0) rainbow (3.0.0)
rake (13.0.1) rake (13.0.1)
rdf (3.1.2) rdf (3.1.4)
hamster (~> 3.0) hamster (~> 3.0)
link_header (~> 0.0, >= 0.0.8) link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.4.0) rdf-normalize (0.4.0)
rdf (~> 3.1) rdf (~> 3.1)
redis (4.1.4) redis (4.2.1)
redis-actionpack (5.2.0) redis-actionpack (5.2.0)
actionpack (>= 5, < 7) actionpack (>= 5, < 7)
redis-rack (>= 2.1.0, < 3) redis-rack (>= 2.1.0, < 3)
@ -507,9 +506,9 @@ GEM
redis-actionpack (>= 5.0, < 6) redis-actionpack (>= 5.0, < 6)
redis-activesupport (>= 5.0, < 6) redis-activesupport (>= 5.0, < 6)
redis-store (>= 1.2, < 2) redis-store (>= 1.2, < 2)
redis-store (1.8.2) redis-store (1.9.0)
redis (>= 4, < 5) redis (>= 4, < 5)
regexp_parser (1.7.0) regexp_parser (1.7.1)
request_store (1.5.0) request_store (1.5.0)
rack (>= 1.4) rack (>= 1.4)
responders (3.0.1) responders (3.0.1)
@ -538,42 +537,42 @@ GEM
rspec-expectations (~> 3.9) rspec-expectations (~> 3.9)
rspec-mocks (~> 3.9) rspec-mocks (~> 3.9)
rspec-support (~> 3.9) rspec-support (~> 3.9)
rspec-sidekiq (3.0.3) rspec-sidekiq (3.1.0)
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.9.3) rspec-support (3.9.3)
rspec_junit_formatter (0.4.1) rspec_junit_formatter (0.4.1)
rspec-core (>= 2, < 4, != 2.12.0) rspec-core (>= 2, < 4, != 2.12.0)
rubocop (0.84.0) rubocop (0.86.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 2.7.0.1) parser (>= 2.7.0.1)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.7)
rexml rexml
rubocop-ast (>= 0.0.3) rubocop-ast (>= 0.0.3, < 1.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 2.0) unicode-display_width (>= 1.4.0, < 2.0)
rubocop-ast (0.0.3) rubocop-ast (0.1.0)
parser (>= 2.7.0.1) parser (>= 2.7.0.1)
rubocop-rails (2.5.2) rubocop-rails (2.6.0)
activesupport activesupport (>= 4.2.0)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 0.72.0) rubocop (>= 0.82.0)
ruby-progressbar (1.10.1) ruby-progressbar (1.10.1)
ruby-saml (1.11.0) ruby-saml (1.11.0)
nokogiri (>= 1.5.10) nokogiri (>= 1.5.10)
rufus-scheduler (3.6.0) rufus-scheduler (3.6.0)
fugit (~> 1.1, >= 1.1.6) fugit (~> 1.1, >= 1.1.6)
safe_yaml (1.0.5) safe_yaml (1.0.5)
sanitize (5.1.0) sanitize (5.2.1)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.8.0) nokogiri (>= 1.8.0)
nokogumbo (~> 2.0) nokogumbo (~> 2.0)
semantic_range (2.3.0) semantic_range (2.3.0)
sidekiq (6.0.7) sidekiq (6.1.0)
connection_pool (>= 2.2.2) connection_pool (>= 2.2.2)
rack (~> 2.0) rack (~> 2.0)
rack-protection (>= 2.0.0) redis (>= 4.2.0)
redis (>= 4.1.0)
sidekiq-bulk (0.2.0) sidekiq-bulk (0.2.0)
sidekiq sidekiq
sidekiq-scheduler (3.0.1) sidekiq-scheduler (3.0.1)
@ -660,7 +659,7 @@ GEM
jwt (~> 2.0) jwt (~> 2.0)
websocket-driver (0.7.2) websocket-driver (0.7.2)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.4) websocket-extensions (0.1.5)
wisper (2.0.1) wisper (2.0.1)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
@ -673,7 +672,7 @@ DEPENDENCIES
active_record_query_trace (~> 1.7) active_record_query_trace (~> 1.7)
addressable (~> 2.7) addressable (~> 2.7)
annotate (~> 3.1) annotate (~> 3.1)
aws-sdk-s3 (~> 1.66) aws-sdk-s3 (~> 1.73)
better_errors (~> 2.7) better_errors (~> 2.7)
binding_of_caller (~> 0.7) binding_of_caller (~> 0.7)
blurhash (~> 0.1) blurhash (~> 0.1)
@ -681,16 +680,17 @@ DEPENDENCIES
brakeman (~> 4.8) brakeman (~> 4.8)
browser browser
bullet (~> 6.1) bullet (~> 6.1)
bundler-audit (~> 0.6) bundler-audit (~> 0.7)
capistrano (~> 3.14) capistrano (~> 3.14)
capistrano-rails (~> 1.5) capistrano-rails (~> 1.5)
capistrano-rbenv (~> 2.1) capistrano-rbenv (~> 2.1)
capistrano-yarn (~> 2.0) capistrano-yarn (~> 2.0)
capybara (~> 3.32) capybara (~> 3.33)
charlock_holmes (~> 0.7.7) charlock_holmes (~> 0.7.7)
chewy (~> 5.1) chewy (~> 5.1)
cld3 (~> 3.3.0) cld3 (~> 3.3.0)
climate_control (~> 0.2) climate_control (~> 0.2)
color_diff (~> 0.1)
concurrent-ruby concurrent-ruby
connection_pool connection_pool
devise (~> 4.7) devise (~> 4.7)
@ -702,7 +702,7 @@ DEPENDENCIES
e2mmap (~> 0.1.0) e2mmap (~> 0.1.0)
ed25519 (~> 1.2) ed25519 (~> 1.2)
fabrication (~> 2.21) fabrication (~> 2.21)
faker (~> 2.12) faker (~> 2.13)
fast_blank (~> 1.0) fast_blank (~> 1.0)
fastimage fastimage
fog-core (<= 2.1.0) fog-core (<= 2.1.0)
@ -716,7 +716,7 @@ DEPENDENCIES
http (~> 4.4) http (~> 4.4)
http_accept_language (~> 2.1) http_accept_language (~> 2.1)
http_parser.rb (~> 0.6)! http_parser.rb (~> 0.6)!
httplog (~> 1.4.2) httplog (~> 1.4.3)
i18n-tasks (~> 0.9) i18n-tasks (~> 0.9)
idn-ruby idn-ruby
iso-639 iso-639
@ -744,7 +744,7 @@ DEPENDENCIES
paperclip (~> 6.0) paperclip (~> 6.0)
paperclip-av-transcoder (~> 0.6) paperclip-av-transcoder (~> 0.6)
parallel (~> 1.19) parallel (~> 1.19)
parallel_tests (~> 2.32) parallel_tests (~> 3.0)
parslet parslet
pg (~> 1.2) pg (~> 1.2)
pghero (~> 2.5) pghero (~> 2.5)
@ -756,7 +756,7 @@ DEPENDENCIES
pry-rails (~> 0.3) pry-rails (~> 0.3)
puma (~> 4.3) puma (~> 4.3)
pundit (~> 2.1) pundit (~> 2.1)
rack (~> 2.2.2) rack (~> 2.2.3)
rack-attack (~> 6.3) rack-attack (~> 6.3)
rack-cors (~> 1.1) rack-cors (~> 1.1)
rails (~> 5.2.4.3) rails (~> 5.2.4.3)
@ -764,17 +764,17 @@ DEPENDENCIES
rails-i18n (~> 5.1) rails-i18n (~> 5.1)
rails-settings-cached (~> 0.6) rails-settings-cached (~> 0.6)
rdf-normalize (~> 0.4) rdf-normalize (~> 0.4)
redis (~> 4.1) redis (~> 4.2)
redis-namespace (~> 1.7) redis-namespace (~> 1.7)
redis-rails (~> 5.0) redis-rails (~> 5.0)
rqrcode (~> 1.1) rqrcode (~> 1.1)
rspec-rails (~> 4.0) rspec-rails (~> 4.0)
rspec-sidekiq (~> 3.0) rspec-sidekiq (~> 3.1)
rspec_junit_formatter (~> 0.4) rspec_junit_formatter (~> 0.4)
rubocop (~> 0.84) rubocop (~> 0.86)
rubocop-rails (~> 2.5) rubocop-rails (~> 2.6)
ruby-progressbar (~> 1.10) ruby-progressbar (~> 1.10)
sanitize (~> 5.1) sanitize (~> 5.2)
sidekiq (~> 6.0) sidekiq (~> 6.0)
sidekiq-bulk (~> 0.2.0) sidekiq-bulk (~> 0.2.0)
sidekiq-scheduler (~> 3.0) sidekiq-scheduler (~> 3.0)

View File

@ -1,7 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class AccountsController < ApplicationController class AccountsController < ApplicationController
PAGE_SIZE = 20 PAGE_SIZE = 20
PAGE_SIZE_MAX = 200
include AccountControllerConcern include AccountControllerConcern
include SignatureAuthentication include SignatureAuthentication
@ -10,7 +11,7 @@ class AccountsController < ApplicationController
before_action :set_body_classes before_action :set_body_classes
skip_around_action :set_locale, if: -> { [:json, :rss].include?(request.format&.to_sym) } skip_around_action :set_locale, if: -> { [:json, :rss].include?(request.format&.to_sym) }
skip_before_action :require_functional! skip_before_action :require_functional!, unless: :whitelist_mode?
def show def show
respond_to do |format| respond_to do |format|
@ -40,7 +41,8 @@ class AccountsController < ApplicationController
format.rss do format.rss do
expires_in 1.minute, public: true expires_in 1.minute, public: true
@statuses = filtered_statuses.without_reblogs.limit(PAGE_SIZE) limit = params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE
@statuses = filtered_statuses.without_reblogs.limit(limit)
@statuses = cache_collection(@statuses, Status) @statuses = cache_collection(@statuses, Status)
render xml: RSS::AccountSerializer.render(@account, @statuses, params[:tag]) render xml: RSS::AccountSerializer.render(@account, @statuses, params[:tag])
end end

View File

@ -33,6 +33,8 @@ module Admin
@form.save @form.save
rescue ActionController::ParameterMissing rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.accounts.no_account_selected') flash[:alert] = I18n.t('admin.accounts.no_account_selected')
rescue Mastodon::NotPermittedError
flash[:alert] = I18n.t('admin.custom_emojis.not_permitted')
ensure ensure
redirect_to admin_custom_emojis_path(filter_params) redirect_to admin_custom_emojis_path(filter_params)
end end

View File

@ -7,7 +7,7 @@ class Api::BaseController < ApplicationController
include RateLimitHeaders include RateLimitHeaders
skip_before_action :store_current_location skip_before_action :store_current_location
skip_before_action :require_functional! skip_before_action :require_functional!, unless: :whitelist_mode?
before_action :require_authenticated_user!, if: :disallow_unauthenticated_api_access? before_action :require_authenticated_user!, if: :disallow_unauthenticated_api_access?
before_action :set_cache_headers before_action :set_cache_headers

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
class Api::V1::Accounts::NotesController < Api::BaseController
include Authorization
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }
before_action :require_user!
before_action :set_account
def create
if params[:comment].blank?
AccountNote.find_by(account: current_account, target_account: @account)&.destroy
else
@note = AccountNote.find_or_initialize_by(account: current_account, target_account: @account)
@note.comment = params[:comment]
@note.save! if @note.changed?
end
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter
end
private
def set_account
@account = Account.find(params[:account_id])
end
def relationships_presenter
AccountRelationshipsPresenter.new([@account.id], current_user.account_id)
end
end

View File

@ -39,7 +39,7 @@ class Api::V1::MediaController < Api::BaseController
end end
def media_attachment_params def media_attachment_params
params.permit(:file, :description, :focus) params.permit(:file, :thumbnail, :description, :focus)
end end
def file_type_error def file_type_error

View File

@ -8,7 +8,10 @@ class Auth::PasswordsController < Devise::PasswordsController
def update def update
super do |resource| super do |resource|
resource.session_activations.destroy_all if resource.errors.empty? if resource.errors.empty?
resource.session_activations.destroy_all
resource.forget_me!
end
end end
end end

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class Auth::RegistrationsController < Devise::RegistrationsController class Auth::RegistrationsController < Devise::RegistrationsController
include Devise::Controllers::Rememberable
layout :determine_layout layout :determine_layout
before_action :set_invite, only: [:new, :create] before_action :set_invite, only: [:new, :create]
@ -24,7 +26,11 @@ class Auth::RegistrationsController < Devise::RegistrationsController
def update def update
super do |resource| super do |resource|
resource.clear_other_sessions(current_session.session_id) if resource.saved_change_to_encrypted_password? if resource.saved_change_to_encrypted_password?
resource.clear_other_sessions(current_session.session_id)
resource.forget_me!
remember_me(resource)
end
end end
end end

View File

@ -8,7 +8,8 @@ class Auth::SessionsController < Devise::SessionsController
skip_before_action :require_no_authentication, only: [:create] skip_before_action :require_no_authentication, only: [:create]
skip_before_action :require_functional! skip_before_action :require_functional!
prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create] include TwoFactorAuthenticationConcern
include SignInTokenAuthenticationConcern
before_action :set_instance_presenter, only: [:new] before_action :set_instance_presenter, only: [:new]
before_action :set_body_classes before_action :set_body_classes
@ -39,8 +40,8 @@ class Auth::SessionsController < Devise::SessionsController
protected protected
def find_user def find_user
if session[:otp_user_id] if session[:attempt_user_id]
User.find(session[:otp_user_id]) User.find(session[:attempt_user_id])
else else
user = User.authenticate_with_ldap(user_params) if Devise.ldap_authentication user = User.authenticate_with_ldap(user_params) if Devise.ldap_authentication
user ||= User.authenticate_with_pam(user_params) if Devise.pam_authentication user ||= User.authenticate_with_pam(user_params) if Devise.pam_authentication
@ -49,7 +50,7 @@ class Auth::SessionsController < Devise::SessionsController
end end
def user_params def user_params
params.require(:user).permit(:email, :password, :otp_attempt) params.require(:user).permit(:email, :password, :otp_attempt, :sign_in_token_attempt)
end end
def after_sign_in_path_for(resource) def after_sign_in_path_for(resource)
@ -70,47 +71,6 @@ class Auth::SessionsController < Devise::SessionsController
super super
end end
def two_factor_enabled?
find_user&.otp_required_for_login?
end
def valid_otp_attempt?(user)
user.validate_and_consume_otp!(user_params[:otp_attempt]) ||
user.invalidate_otp_backup_code!(user_params[:otp_attempt])
rescue OpenSSL::Cipher::CipherError
false
end
def authenticate_with_two_factor
user = self.resource = find_user
if user_params[:otp_attempt].present? && session[:otp_user_id]
authenticate_with_two_factor_via_otp(user)
elsif user.present? && (user.encrypted_password.blank? || user.valid_password?(user_params[:password]))
# If encrypted_password is blank, we got the user from LDAP or PAM,
# so credentials are already valid
prompt_for_two_factor(user)
end
end
def authenticate_with_two_factor_via_otp(user)
if valid_otp_attempt?(user)
session.delete(:otp_user_id)
remember_me(user)
sign_in(user)
else
flash.now[:alert] = I18n.t('users.invalid_otp_token')
prompt_for_two_factor(user)
end
end
def prompt_for_two_factor(user)
session[:otp_user_id] = user.id
@body_classes = 'lighter'
render :two_factor
end
def require_no_authentication def require_no_authentication
super super
# Delete flash message that isn't entirely useful and may be confusing in # Delete flash message that isn't entirely useful and may be confusing in

View File

@ -7,8 +7,6 @@ module Localized
around_action :set_locale around_action :set_locale
end end
private
def set_locale def set_locale
locale = current_user.locale if respond_to?(:user_signed_in?) && user_signed_in? locale = current_user.locale if respond_to?(:user_signed_in?) && user_signed_in?
locale ||= session[:locale] ||= default_locale locale ||= session[:locale] ||= default_locale
@ -19,6 +17,8 @@ module Localized
end end
end end
private
def default_locale def default_locale
if ENV['DEFAULT_LOCALE'].present? if ENV['DEFAULT_LOCALE'].present?
I18n.default_locale I18n.default_locale

View File

@ -0,0 +1,51 @@
# frozen_string_literal: true
module SignInTokenAuthenticationConcern
extend ActiveSupport::Concern
included do
prepend_before_action :authenticate_with_sign_in_token, if: :sign_in_token_required?, only: [:create]
end
def sign_in_token_required?
find_user&.suspicious_sign_in?(request.remote_ip)
end
def valid_sign_in_token_attempt?(user)
Devise.secure_compare(user.sign_in_token, user_params[:sign_in_token_attempt])
end
def authenticate_with_sign_in_token
user = self.resource = find_user
if user_params[:sign_in_token_attempt].present? && session[:attempt_user_id]
authenticate_with_sign_in_token_attempt(user)
elsif user.present? && user.external_or_valid_password?(user_params[:password])
prompt_for_sign_in_token(user)
end
end
def authenticate_with_sign_in_token_attempt(user)
if valid_sign_in_token_attempt?(user)
session.delete(:attempt_user_id)
remember_me(user)
sign_in(user)
else
flash.now[:alert] = I18n.t('users.invalid_sign_in_token')
prompt_for_sign_in_token(user)
end
end
def prompt_for_sign_in_token(user)
if user.sign_in_token_expired?
user.generate_sign_in_token && user.save
UserMailer.sign_in_token(user, request.remote_ip, request.user_agent, Time.now.utc.to_s).deliver_later!
end
set_locale do
session[:attempt_user_id] = user.id
@body_classes = 'lighter'
render :sign_in_token
end
end
end

View File

@ -0,0 +1,49 @@
# frozen_string_literal: true
module TwoFactorAuthenticationConcern
extend ActiveSupport::Concern
included do
prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create]
end
def two_factor_enabled?
find_user&.otp_required_for_login?
end
def valid_otp_attempt?(user)
user.validate_and_consume_otp!(user_params[:otp_attempt]) ||
user.invalidate_otp_backup_code!(user_params[:otp_attempt])
rescue OpenSSL::Cipher::CipherError
false
end
def authenticate_with_two_factor
user = self.resource = find_user
if user_params[:otp_attempt].present? && session[:attempt_user_id]
authenticate_with_two_factor_attempt(user)
elsif user.present? && user.external_or_valid_password?(user_params[:password])
prompt_for_two_factor(user)
end
end
def authenticate_with_two_factor_attempt(user)
if valid_otp_attempt?(user)
session.delete(:attempt_user_id)
remember_me(user)
sign_in(user)
else
flash.now[:alert] = I18n.t('users.invalid_otp_token')
prompt_for_two_factor(user)
end
end
def prompt_for_two_factor(user)
set_locale do
session[:attempt_user_id] = user.id
@body_classes = 'lighter'
render :two_factor
end
end
end

View File

@ -9,7 +9,7 @@ class DirectoriesController < ApplicationController
before_action :set_tag, only: :show before_action :set_tag, only: :show
before_action :set_accounts before_action :set_accounts
skip_before_action :require_functional! skip_before_action :require_functional!, unless: :whitelist_mode?
def index def index
render :index render :index

View File

@ -8,7 +8,7 @@ class FollowerAccountsController < ApplicationController
before_action :set_cache_headers before_action :set_cache_headers
skip_around_action :set_locale, if: -> { request.format == :json } skip_around_action :set_locale, if: -> { request.format == :json }
skip_before_action :require_functional! skip_before_action :require_functional!, unless: :whitelist_mode?
def index def index
respond_to do |format| respond_to do |format|

View File

@ -8,7 +8,7 @@ class FollowingAccountsController < ApplicationController
before_action :set_cache_headers before_action :set_cache_headers
skip_around_action :set_locale, if: -> { request.format == :json } skip_around_action :set_locale, if: -> { request.format == :json }
skip_before_action :require_functional! skip_before_action :require_functional!, unless: :whitelist_mode?
def index def index
respond_to do |format| respond_to do |format|

View File

@ -1,6 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class HomeController < ApplicationController class HomeController < ApplicationController
before_action :redirect_unauthenticated_to_permalinks!
before_action :authenticate_user! before_action :authenticate_user!
before_action :set_referrer_policy_header before_action :set_referrer_policy_header
@ -10,7 +11,7 @@ class HomeController < ApplicationController
private private
def authenticate_user! def redirect_unauthenticated_to_permalinks!
return if user_signed_in? return if user_signed_in?
matches = request.path.match(/\A\/web\/(statuses|accounts)\/([\d]+)\z/) matches = request.path.match(/\A\/web\/(statuses|accounts)\/([\d]+)\z/)
@ -35,6 +36,7 @@ class HomeController < ApplicationController
end end
matches = request.path.match(%r{\A/web/timelines/tag/(?<tag>.+)\z}) matches = request.path.match(%r{\A/web/timelines/tag/(?<tag>.+)\z})
redirect_to(matches ? tag_path(CGI.unescape(matches[:tag])) : default_redirect_path) redirect_to(matches ? tag_path(CGI.unescape(matches[:tag])) : default_redirect_path)
end end

View File

@ -4,7 +4,7 @@ class MediaController < ApplicationController
include Authorization include Authorization
skip_before_action :store_current_location skip_before_action :store_current_location
skip_before_action :require_functional! skip_before_action :require_functional!, unless: :whitelist_mode?
before_action :authenticate_user!, if: :whitelist_mode? before_action :authenticate_user!, if: :whitelist_mode?
before_action :set_media_attachment before_action :set_media_attachment

View File

@ -2,6 +2,7 @@
class MediaProxyController < ApplicationController class MediaProxyController < ApplicationController
include RoutingHelper include RoutingHelper
include Authorization
skip_before_action :store_current_location skip_before_action :store_current_location
skip_before_action :require_functional! skip_before_action :require_functional!
@ -10,12 +11,14 @@ class MediaProxyController < ApplicationController
rescue_from ActiveRecord::RecordInvalid, with: :not_found rescue_from ActiveRecord::RecordInvalid, with: :not_found
rescue_from Mastodon::UnexpectedResponseError, with: :not_found rescue_from Mastodon::UnexpectedResponseError, with: :not_found
rescue_from Mastodon::NotPermittedError, with: :not_found
rescue_from HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, with: :internal_server_error rescue_from HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, with: :internal_server_error
def show def show
RedisLock.acquire(lock_options) do |lock| RedisLock.acquire(lock_options) do |lock|
if lock.acquired? if lock.acquired?
@media_attachment = MediaAttachment.remote.find(params[:id]) @media_attachment = MediaAttachment.remote.attached.find(params[:id])
authorize @media_attachment.status, :show?
redownload! if @media_attachment.needs_redownload? && !reject_media? redownload! if @media_attachment.needs_redownload? && !reject_media?
else else
raise Mastodon::RaceConditionError raise Mastodon::RaceConditionError
@ -28,8 +31,8 @@ class MediaProxyController < ApplicationController
private private
def redownload! def redownload!
@media_attachment.file_remote_url = @media_attachment.remote_url @media_attachment.download_file!
@media_attachment.created_at = Time.now.utc @media_attachment.created_at = Time.now.utc
@media_attachment.save! @media_attachment.save!
end end

View File

@ -10,7 +10,7 @@ class RemoteInteractionController < ApplicationController
before_action :set_status before_action :set_status
before_action :set_body_classes before_action :set_body_classes
skip_before_action :require_functional! skip_before_action :require_functional!, unless: :whitelist_mode?
def new def new
@remote_follow = RemoteFollow.new(session_params) @remote_follow = RemoteFollow.new(session_params)

View File

@ -7,13 +7,8 @@ module Settings
before_action :set_picture before_action :set_picture
def destroy def destroy
if valid_picture if valid_picture?
account_params = { msg = I18n.t('generic.changes_saved_msg') if UpdateAccountService.new.call(@account, { @picture => nil, "#{@picture}_remote_url" => '' })
@picture => nil,
(@picture + '_remote_url') => nil,
}
msg = UpdateAccountService.new.call(@account, account_params) ? I18n.t('generic.changes_saved_msg') : nil
redirect_to settings_profile_path, notice: msg, status: 303 redirect_to settings_profile_path, notice: msg, status: 303
else else
bad_request bad_request
@ -30,8 +25,8 @@ module Settings
@picture = params[:id] @picture = params[:id]
end end
def valid_picture def valid_picture?
@picture == 'avatar' || @picture == 'header' %w(avatar header).include?(@picture)
end end
end end
end end

View File

@ -19,7 +19,7 @@ class StatusesController < ApplicationController
before_action :set_autoplay, only: :embed before_action :set_autoplay, only: :embed
skip_around_action :set_locale, if: -> { request.format == :json } skip_around_action :set_locale, if: -> { request.format == :json }
skip_before_action :require_functional!, only: [:show, :embed] skip_before_action :require_functional!, only: [:show, :embed], unless: :whitelist_mode?
content_security_policy only: :embed do |p| content_security_policy only: :embed do |p|
p.frame_ancestors(false) p.frame_ancestors(false)

View File

@ -3,7 +3,8 @@
class TagsController < ApplicationController class TagsController < ApplicationController
include SignatureVerification include SignatureVerification
PAGE_SIZE = 20 PAGE_SIZE = 20
PAGE_SIZE_MAX = 200
layout 'public' layout 'public'
@ -14,7 +15,7 @@ class TagsController < ApplicationController
before_action :set_body_classes before_action :set_body_classes
before_action :set_instance_presenter before_action :set_instance_presenter
skip_before_action :require_functional! skip_before_action :require_functional!, unless: :whitelist_mode?
def show def show
respond_to do |format| respond_to do |format|
@ -25,6 +26,7 @@ class TagsController < ApplicationController
format.rss do format.rss do
expires_in 0, public: true expires_in 0, public: true
limit = params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE
@statuses = HashtagQueryService.new.call(@tag, filter_params, nil, @local).limit(PAGE_SIZE) @statuses = HashtagQueryService.new.call(@tag, filter_params, nil, @local).limit(PAGE_SIZE)
@statuses = cache_collection(@statuses, Status) @statuses = cache_collection(@statuses, Status)

View File

@ -77,6 +77,18 @@ 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 visibility_icon(status)
if status.public_visibility?
fa_icon('globe', title: I18n.t('statuses.visibilities.public'))
elsif status.unlisted_visibility?
fa_icon('unlock', title: I18n.t('statuses.visibilities.unlisted'))
elsif status.private_visibility? || status.limited_visibility?
fa_icon('lock', title: I18n.t('statuses.visibilities.private'))
elsif status.direct_visibility?
fa_icon('envelope', title: I18n.t('statuses.visibilities.direct'))
end
end
def custom_emoji_tag(custom_emoji, animate = true) def custom_emoji_tag(custom_emoji, animate = true)
if animate if animate
image_tag(custom_emoji.image.url, class: 'emojione', alt: ":#{custom_emoji.shortcode}:") image_tag(custom_emoji.image.url, class: 'emojione', alt: ":#{custom_emoji.shortcode}:")
@ -136,6 +148,11 @@ module ApplicationHelper
text: [params[:title], params[:text], params[:url]].compact.join(' '), text: [params[:title], params[:text], params[:url]].compact.join(' '),
} }
permit_visibilities = %w(public unlisted private direct)
default_privacy = current_account&.user&.setting_default_privacy
permit_visibilities.shift(permit_visibilities.index(default_privacy) + 1) if default_privacy.present?
state_params[:visibility] = params[:visibility] if permit_visibilities.include? params[:visibility]
if user_signed_in? if user_signed_in?
state_params[:settings] = state_params[:settings].merge(Web::Setting.find_by(user: current_user)&.data || {}) state_params[:settings] = state_params[:settings].merge(Web::Setting.find_by(user: current_user)&.data || {})
state_params[:push_subscription] = current_account.user.web_push_subscription(current_session) state_params[:push_subscription] = current_account.user.web_push_subscription(current_session)

View File

@ -15,11 +15,13 @@ module StatusesHelper
end end
def media_summary(status) def media_summary(status)
attachments = { image: 0, video: 0 } attachments = { image: 0, video: 0, audio: 0 }
status.media_attachments.each do |media| status.media_attachments.each do |media|
if media.video? if media.video?
attachments[:video] += 1 attachments[:video] += 1
elsif media.audio?
attachments[:audio] += 1
else else
attachments[:image] += 1 attachments[:image] += 1
end end

View File

@ -1,5 +1,16 @@
# frozen_string_literal: true # frozen_string_literal: true
# Monkey-patch on monkey-patch.
# Because it conflicts with the request.rb patch.
class HTTP::Timeout::PerOperationOriginal < HTTP::Timeout::PerOperation
def connect(socket_class, host, port, nodelay = false)
::Timeout.timeout(@connect_timeout, HTTP::TimeoutError) do
@socket = socket_class.open(host, port)
@socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay
end
end
end
module WebfingerHelper module WebfingerHelper
def webfinger!(uri) def webfinger!(uri)
hidden_service_uri = /\.(onion|i2p)(:\d+)?$/.match(uri) hidden_service_uri = /\.(onion|i2p)(:\d+)?$/.match(uri)
@ -12,6 +23,14 @@ module WebfingerHelper
headers: { headers: {
'User-Agent': Mastodon::Version.user_agent, 'User-Agent': Mastodon::Version.user_agent,
}, },
timeout_class: HTTP::Timeout::PerOperationOriginal,
timeout_options: {
write_timeout: 10,
connect_timeout: 5,
read_timeout: 10,
},
} }
Goldfinger::Client.new(uri, opts.merge(Rails.configuration.x.http_client_proxy)).finger Goldfinger::Client.new(uri, opts.merge(Rails.configuration.x.http_client_proxy)).finger

View File

@ -0,0 +1,37 @@
import api from '../api';
export const ACCOUNT_NOTE_SUBMIT_REQUEST = 'ACCOUNT_NOTE_SUBMIT_REQUEST';
export const ACCOUNT_NOTE_SUBMIT_SUCCESS = 'ACCOUNT_NOTE_SUBMIT_SUCCESS';
export const ACCOUNT_NOTE_SUBMIT_FAIL = 'ACCOUNT_NOTE_SUBMIT_FAIL';
export function submitAccountNote(id, value) {
return (dispatch, getState) => {
dispatch(submitAccountNoteRequest());
api(getState).post(`/api/v1/accounts/${id}/note`, {
comment: value,
}).then(response => {
dispatch(submitAccountNoteSuccess(response.data));
}).catch(error => dispatch(submitAccountNoteFail(error)));
};
};
export function submitAccountNoteRequest() {
return {
type: ACCOUNT_NOTE_SUBMIT_REQUEST,
};
};
export function submitAccountNoteSuccess(relationship) {
return {
type: ACCOUNT_NOTE_SUBMIT_SUCCESS,
relationship,
};
};
export function submitAccountNoteFail(error) {
return {
type: ACCOUNT_NOTE_SUBMIT_FAIL,
error,
};
};

View File

@ -28,6 +28,11 @@ export const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL';
export const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS'; export const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS';
export const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO'; export const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO';
export const THUMBNAIL_UPLOAD_REQUEST = 'THUMBNAIL_UPLOAD_REQUEST';
export const THUMBNAIL_UPLOAD_SUCCESS = 'THUMBNAIL_UPLOAD_SUCCESS';
export const THUMBNAIL_UPLOAD_FAIL = 'THUMBNAIL_UPLOAD_FAIL';
export const THUMBNAIL_UPLOAD_PROGRESS = 'THUMBNAIL_UPLOAD_PROGRESS';
export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR'; export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR';
export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY'; export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT'; export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT';
@ -260,6 +265,49 @@ export function uploadCompose(files) {
}; };
}; };
export const uploadThumbnail = (id, file) => (dispatch, getState) => {
dispatch(uploadThumbnailRequest());
const total = file.size;
const data = new FormData();
data.append('thumbnail', file);
api(getState).put(`/api/v1/media/${id}`, data, {
onUploadProgress: ({ loaded }) => {
dispatch(uploadThumbnailProgress(loaded, total));
},
}).then(({ data }) => {
dispatch(uploadThumbnailSuccess(data));
}).catch(error => {
dispatch(uploadThumbnailFail(id, error));
});
};
export const uploadThumbnailRequest = () => ({
type: THUMBNAIL_UPLOAD_REQUEST,
skipLoading: true,
});
export const uploadThumbnailProgress = (loaded, total) => ({
type: THUMBNAIL_UPLOAD_PROGRESS,
loaded,
total,
skipLoading: true,
});
export const uploadThumbnailSuccess = media => ({
type: THUMBNAIL_UPLOAD_SUCCESS,
media,
skipLoading: true,
});
export const uploadThumbnailFail = error => ({
type: THUMBNAIL_UPLOAD_FAIL,
error,
skipLoading: true,
});
export function changeUploadCompose(id, params) { export function changeUploadCompose(id, params) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(changeUploadComposeRequest()); dispatch(changeUploadComposeRequest());
@ -278,6 +326,7 @@ export function changeUploadComposeRequest() {
skipLoading: true, skipLoading: true,
}; };
}; };
export function changeUploadComposeSuccess(media) { export function changeUploadComposeSuccess(media) {
return { return {
type: COMPOSE_UPLOAD_CHANGE_SUCCESS, type: COMPOSE_UPLOAD_CHANGE_SUCCESS,

View File

@ -12,7 +12,7 @@ const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => {
export function searchTextFromRawStatus (status) { export function searchTextFromRawStatus (status) {
const spoilerText = status.spoiler_text || ''; const spoilerText = status.spoiler_text || '';
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n'); const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
return domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; return domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
} }

View File

@ -1,4 +1,4 @@
import { shallow } from 'enzyme'; import { render, fireEvent, screen } from '@testing-library/react';
import React from 'react'; import React from 'react';
import renderer from 'react-test-renderer'; import renderer from 'react-test-renderer';
import Button from '../button'; import Button from '../button';
@ -21,16 +21,16 @@ describe('<Button />', () => {
it('handles click events using the given handler', () => { it('handles click events using the given handler', () => {
const handler = jest.fn(); const handler = jest.fn();
const button = shallow(<Button onClick={handler} />); render(<Button onClick={handler}>button</Button>);
button.find('button').simulate('click'); fireEvent.click(screen.getByText('button'));
expect(handler.mock.calls.length).toEqual(1); expect(handler.mock.calls.length).toEqual(1);
}); });
it('does not handle click events if props.disabled given', () => { it('does not handle click events if props.disabled given', () => {
const handler = jest.fn(); const handler = jest.fn();
const button = shallow(<Button onClick={handler} disabled />); render(<Button onClick={handler} disabled>button</Button>);
button.find('button').simulate('click'); fireEvent.click(screen.getByText('button'));
expect(handler.mock.calls.length).toEqual(0); expect(handler.mock.calls.length).toEqual(0);
}); });

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { shortNumberFormat } from 'mastodon/utils/numbers'; import ShortNumber from 'mastodon/components/short_number';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
export default class AutosuggestHashtag extends React.PureComponent { export default class AutosuggestHashtag extends React.PureComponent {
@ -13,14 +13,28 @@ export default class AutosuggestHashtag extends React.PureComponent {
}).isRequired, }).isRequired,
}; };
render () { render() {
const { tag } = this.props; const { tag } = this.props;
const weeklyUses = tag.history && shortNumberFormat(tag.history.reduce((total, day) => total + (day.uses * 1), 0)); const weeklyUses = tag.history && (
<ShortNumber
value={tag.history.reduce((total, day) => total + day.uses * 1, 0)}
/>
);
return ( return (
<div className='autosuggest-hashtag'> <div className='autosuggest-hashtag'>
<div className='autosuggest-hashtag__name'>#<strong>{tag.name}</strong></div> <div className='autosuggest-hashtag__name'>
{tag.history !== undefined && <div className='autosuggest-hashtag__uses'><FormattedMessage id='autosuggest_hashtag.per_week' defaultMessage='{count} per week' values={{ count: weeklyUses }} /></div>} #<strong>{tag.name}</strong>
</div>
{tag.history !== undefined && (
<div className='autosuggest-hashtag__uses'>
<FormattedMessage
id='autosuggest_hashtag.per_week'
defaultMessage='{count} per week'
values={{ count: weeklyUses }}
/>
</div>
)}
</div> </div>
); );
} }

View File

@ -0,0 +1,62 @@
// @ts-check
import React from 'react';
import { FormattedMessage } from 'react-intl';
/**
* Returns custom renderer for one of the common counter types
*
* @param {"statuses" | "following" | "followers"} counterType
* Type of the counter
* @param {boolean} isBold Whether display number must be displayed in bold
* @returns {(displayNumber: JSX.Element, pluralReady: number) => JSX.Element}
* Renderer function
* @throws If counterType is not covered by this function
*/
export function counterRenderer(counterType, isBold = true) {
/**
* @type {(displayNumber: JSX.Element) => JSX.Element}
*/
const renderCounter = isBold
? (displayNumber) => <strong>{displayNumber}</strong>
: (displayNumber) => displayNumber;
switch (counterType) {
case 'statuses': {
return (displayNumber, pluralReady) => (
<FormattedMessage
id='account.statuses_counter'
defaultMessage='{count, plural, one {{counter} Toot} other {{counter} Toots}}'
values={{
count: pluralReady,
counter: renderCounter(displayNumber),
}}
/>
);
}
case 'following': {
return (displayNumber, pluralReady) => (
<FormattedMessage
id='account.following_counter'
defaultMessage='{count, plural, other {{counter} Following}}'
values={{
count: pluralReady,
counter: renderCounter(displayNumber),
}}
/>
);
}
case 'followers': {
return (displayNumber, pluralReady) => (
<FormattedMessage
id='account.followers_counter'
defaultMessage='{count, plural, one {{counter} Follower} other {{counter} Followers}}'
values={{
count: pluralReady,
counter: renderCounter(displayNumber),
}}
/>
);
}
default: throw Error(`Incorrect counter name: ${counterType}. Ensure it accepted by commonCounter function`);
}
}

View File

@ -1,26 +1,65 @@
// @ts-check
import React from 'react'; import React from 'react';
import { Sparklines, SparklinesCurve } from 'react-sparklines'; import { Sparklines, SparklinesCurve } from 'react-sparklines';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import Permalink from './permalink'; import Permalink from './permalink';
import { shortNumberFormat } from '../utils/numbers'; import ShortNumber from 'mastodon/components/short_number';
/**
* Used to render counter of how much people are talking about hashtag
*
* @type {(displayNumber: JSX.Element, pluralReady: number) => JSX.Element}
*/
const accountsCountRenderer = (displayNumber, pluralReady) => (
<FormattedMessage
id='trends.counter_by_accounts'
defaultMessage='{count, plural, one {{counter} person} other {{counter} people}} talking'
values={{
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
}}
/>
);
const Hashtag = ({ hashtag }) => ( const Hashtag = ({ hashtag }) => (
<div className='trends__item'> <div className='trends__item'>
<div className='trends__item__name'> <div className='trends__item__name'>
<Permalink href={hashtag.get('url')} to={`/timelines/tag/${hashtag.get('name')}`}> <Permalink
href={hashtag.get('url')}
to={`/timelines/tag/${hashtag.get('name')}`}
>
#<span>{hashtag.get('name')}</span> #<span>{hashtag.get('name')}</span>
</Permalink> </Permalink>
<FormattedMessage id='trends.count_by_accounts' defaultMessage='{count} {rawCount, plural, one {person} other {people}} talking' values={{ rawCount: hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1, count: <strong>{shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1)}</strong> }} /> <ShortNumber
value={
hashtag.getIn(['history', 0, 'accounts']) * 1 +
hashtag.getIn(['history', 1, 'accounts']) * 1
}
renderer={accountsCountRenderer}
/>
</div> </div>
<div className='trends__item__current'> <div className='trends__item__current'>
{shortNumberFormat(hashtag.getIn(['history', 0, 'uses']) * 1 + hashtag.getIn(['history', 1, 'uses']) * 1)} <ShortNumber
value={
hashtag.getIn(['history', 0, 'uses']) * 1 +
hashtag.getIn(['history', 1, 'uses']) * 1
}
/>
</div> </div>
<div className='trends__item__sparkline'> <div className='trends__item__sparkline'>
<Sparklines width={50} height={28} data={hashtag.get('history').reverse().map(day => day.get('uses')).toArray()}> <Sparklines
width={50}
height={28}
data={hashtag
.get('history')
.reverse()
.map((day) => day.get('uses'))
.toArray()}
>
<SparklinesCurve style={{ fill: 'none' }} /> <SparklinesCurve style={{ fill: 'none' }} />
</Sparklines> </Sparklines>
</div> </div>

View File

@ -8,10 +8,10 @@ import { isIOS } from '../is_mobile';
import classNames from 'classnames'; import classNames from 'classnames';
import { autoPlayGif, cropImages, displayMedia, useBlurhash } from '../initial_state'; import { autoPlayGif, cropImages, displayMedia, useBlurhash } from '../initial_state';
import { decode } from 'blurhash'; import { decode } from 'blurhash';
import { debounce } from 'lodash';
const messages = defineMessages({ const messages = defineMessages({
toggle_visible: { id: 'media_gallery.toggle_visible', toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Hide {number, plural, one {image} other {images}}' },
defaultMessage: 'Hide {number, plural, one {image} other {images}}' },
}); });
class Item extends React.PureComponent { class Item extends React.PureComponent {
@ -267,6 +267,14 @@ class MediaGallery extends React.PureComponent {
width: this.props.defaultWidth, width: this.props.defaultWidth,
}; };
componentDidMount () {
window.addEventListener('resize', this.handleResize, { passive: true });
}
componentWillUnmount () {
window.removeEventListener('resize', this.handleResize);
}
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
if (!is(nextProps.media, this.props.media) && nextProps.visible === undefined) { if (!is(nextProps.media, this.props.media) && nextProps.visible === undefined) {
this.setState({ visible: displayMedia !== 'hide_all' && !nextProps.sensitive || displayMedia === 'show_all' }); this.setState({ visible: displayMedia !== 'hide_all' && !nextProps.sensitive || displayMedia === 'show_all' });
@ -275,6 +283,14 @@ class MediaGallery extends React.PureComponent {
} }
} }
handleResize = debounce(() => {
if (this.node) {
this._setDimensions();
}
}, 250, {
trailing: true,
});
handleOpen = () => { handleOpen = () => {
if (this.props.onToggleVisibility) { if (this.props.onToggleVisibility) {
this.props.onToggleVisibility(); this.props.onToggleVisibility();
@ -287,17 +303,27 @@ class MediaGallery extends React.PureComponent {
this.props.onOpenMedia(this.props.media, index); this.props.onOpenMedia(this.props.media, index);
} }
handleRef = (node) => { handleRef = c => {
if (node) { this.node = c;
// offsetWidth triggers a layout, so only calculate when we need to
if (this.props.cacheWidth) this.props.cacheWidth(node.offsetWidth);
this.setState({ if (this.node) {
width: node.offsetWidth, this._setDimensions();
});
} }
} }
_setDimensions () {
const width = this.node.offsetWidth;
// offsetWidth triggers a layout, so only calculate when we need to
if (this.props.cacheWidth) {
this.props.cacheWidth(width);
}
this.setState({
width: width,
});
}
isFullSizeEligible() { isFullSizeEligible() {
const { media } = this.props; const { media } = this.props;
return media.size === 1 && media.getIn([0, 'meta', 'small', 'aspect']); return media.size === 1 && media.getIn([0, 'meta', 'small', 'aspect']);

View File

@ -66,7 +66,7 @@ export default class ModalRoot extends React.PureComponent {
// immediately selectable, we have to wait for observers to run, as // immediately selectable, we have to wait for observers to run, as
// described in https://github.com/WICG/inert#performance-and-gotchas // described in https://github.com/WICG/inert#performance-and-gotchas
Promise.resolve().then(() => { Promise.resolve().then(() => {
this.activeElement.focus(); this.activeElement.focus({ preventScroll: true });
this.activeElement = null; this.activeElement = null;
}).catch((error) => { }).catch((error) => {
console.error(error); console.error(error);

View File

@ -32,6 +32,7 @@ export default class ScrollableList extends PureComponent {
hasMore: PropTypes.bool, hasMore: PropTypes.bool,
numPending: PropTypes.number, numPending: PropTypes.number,
prepend: PropTypes.node, prepend: PropTypes.node,
append: PropTypes.node,
alwaysPrepend: PropTypes.bool, alwaysPrepend: PropTypes.bool,
emptyMessage: PropTypes.node, emptyMessage: PropTypes.node,
children: PropTypes.node, children: PropTypes.node,
@ -280,7 +281,7 @@ export default class ScrollableList extends PureComponent {
} }
render () { render () {
const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, emptyMessage, onLoadMore } = this.props; const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, emptyMessage, onLoadMore } = this.props;
const { fullscreen } = this.state; const { fullscreen } = this.state;
const childrenCount = React.Children.count(children); const childrenCount = React.Children.count(children);
@ -327,6 +328,8 @@ export default class ScrollableList extends PureComponent {
))} ))}
{loadMore} {loadMore}
{!hasMore && append}
</div> </div>
</div> </div>
); );

View File

@ -0,0 +1,117 @@
import React from 'react';
import PropTypes from 'prop-types';
import { toShortNumber, pluralReady, DECIMAL_UNITS } from '../utils/numbers';
import { FormattedMessage, FormattedNumber } from 'react-intl';
// @ts-check
/**
* @callback ShortNumberRenderer
* @param {JSX.Element} displayNumber Number to display
* @param {number} pluralReady Number used for pluralization
* @returns {JSX.Element} Final render of number
*/
/**
* @typedef {object} ShortNumberProps
* @property {number} value Number to display in short variant
* @property {ShortNumberRenderer} [renderer]
* Custom renderer for numbers, provided as a prop. If another renderer
* passed as a child of this component, this prop won't be used.
* @property {ShortNumberRenderer} [children]
* Custom renderer for numbers, provided as a child. If another renderer
* passed as a prop of this component, this one will be used instead.
*/
/**
* Component that renders short big number to a shorter version
*
* @param {ShortNumberProps} param0 Props for the component
* @returns {JSX.Element} Rendered number
*/
function ShortNumber({ value, renderer, children }) {
const shortNumber = toShortNumber(value);
const [, division] = shortNumber;
// eslint-disable-next-line eqeqeq
if (children != null && renderer != null) {
console.warn('Both renderer prop and renderer as a child provided. This is a mistake and you really should fix that. Only renderer passed as a child will be used.');
}
// eslint-disable-next-line eqeqeq
const customRenderer = children != null ? children : renderer;
const displayNumber = <ShortNumberCounter value={shortNumber} />;
// eslint-disable-next-line eqeqeq
return customRenderer != null
? customRenderer(displayNumber, pluralReady(value, division))
: displayNumber;
}
ShortNumber.propTypes = {
value: PropTypes.number.isRequired,
renderer: PropTypes.func,
children: PropTypes.func,
};
/**
* @typedef {object} ShortNumberCounterProps
* @property {import('../utils/number').ShortNumber} value Short number
*/
/**
* Renders short number into corresponding localizable react fragment
*
* @param {ShortNumberCounterProps} param0 Props for the component
* @returns {JSX.Element} FormattedMessage ready to be embedded in code
*/
function ShortNumberCounter({ value }) {
const [rawNumber, unit, maxFractionDigits = 0] = value;
const count = (
<FormattedNumber
value={rawNumber}
maximumFractionDigits={maxFractionDigits}
/>
);
let values = { count, rawNumber };
switch (unit) {
case DECIMAL_UNITS.THOUSAND: {
return (
<FormattedMessage
id='units.short.thousand'
defaultMessage='{count}K'
values={values}
/>
);
}
case DECIMAL_UNITS.MILLION: {
return (
<FormattedMessage
id='units.short.million'
defaultMessage='{count}M'
values={values}
/>
);
}
case DECIMAL_UNITS.BILLION: {
return (
<FormattedMessage
id='units.short.billion'
defaultMessage='{count}B'
values={values}
/>
);
}
// Not sure if we should go farther - @Sasha-Sorokin
default: return count;
}
}
ShortNumberCounter.propTypes = {
value: PropTypes.arrayOf(PropTypes.number),
};
export default React.memo(ShortNumber);

View File

@ -10,7 +10,7 @@ import StatusContent from './status_content';
import StatusActionBar from './status_action_bar'; import StatusActionBar from './status_action_bar';
import AttachmentList from './attachment_list'; import AttachmentList from './attachment_list';
import Card from '../features/status/components/card'; import Card from '../features/status/components/card';
import { injectIntl, FormattedMessage } from 'react-intl'; import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components'; import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
import { HotKeys } from 'react-hotkeys'; import { HotKeys } from 'react-hotkeys';
@ -51,6 +51,13 @@ export const defaultMediaVisibility = (status) => {
return (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all'); return (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all');
}; };
const messages = defineMessages({
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
});
export default @injectIntl export default @injectIntl
class Status extends ImmutablePureComponent { class Status extends ImmutablePureComponent {
@ -345,9 +352,14 @@ class Status extends ImmutablePureComponent {
<Component <Component
src={attachment.get('url')} src={attachment.get('url')}
alt={attachment.get('description')} alt={attachment.get('description')}
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)} duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
peaks={[0]} width={this.props.cachedMediaWidth}
height={70} height={110}
cacheWidth={this.props.cacheMediaWidth}
/> />
)} )}
</Bundle> </Bundle>
@ -401,6 +413,7 @@ class Status extends ImmutablePureComponent {
compact compact
cacheWidth={this.props.cacheMediaWidth} cacheWidth={this.props.cacheMediaWidth}
defaultWidth={this.props.cachedMediaWidth} defaultWidth={this.props.cachedMediaWidth}
sensitive={status.get('sensitive')}
/> />
); );
} }
@ -413,6 +426,15 @@ class Status extends ImmutablePureComponent {
statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />; statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />;
} }
const visibilityIconInfo = {
'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) },
'unlisted': { icon: 'unlock', text: intl.formatMessage(messages.unlisted_short) },
'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) },
'direct': { icon: 'envelope', text: intl.formatMessage(messages.direct_short) },
};
const visibilityIcon = visibilityIconInfo[status.get('visibility')];
return ( return (
<HotKeys handlers={handlers}> <HotKeys handlers={handlers}>
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}> <div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}>
@ -422,6 +444,7 @@ class Status extends ImmutablePureComponent {
<div className='status__expand' onClick={this.handleExpandClick} role='presentation' /> <div className='status__expand' onClick={this.handleExpandClick} role='presentation' />
<div className='status__info'> <div className='status__info'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'><RelativeTimestamp timestamp={status.get('created_at')} /></a> <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
<span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span>
<a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'> <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
<div className='status__avatar'> <div className='status__avatar'>

View File

@ -237,9 +237,6 @@ class StatusActionBar extends ImmutablePureComponent {
const account = status.get('account'); const account = status.get('account');
let menu = []; let menu = [];
let reblogIcon = 'retweet';
let replyIcon;
let replyTitle;
menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen }); menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
@ -259,10 +256,6 @@ class StatusActionBar extends ImmutablePureComponent {
if (status.getIn(['account', 'id']) === me) { if (status.getIn(['account', 'id']) === me) {
if (publicStatus) { if (publicStatus) {
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
} else {
if (status.get('visibility') === 'private') {
menu.push({ text: intl.formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog_private), action: this.handleReblogClick });
}
} }
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
@ -305,12 +298,8 @@ class StatusActionBar extends ImmutablePureComponent {
} }
} }
if (status.get('visibility') === 'direct') { let replyIcon;
reblogIcon = 'envelope'; let replyTitle;
} else if (status.get('visibility') === 'private') {
reblogIcon = 'lock';
}
if (status.get('in_reply_to_id', null) === null) { if (status.get('in_reply_to_id', null) === null) {
replyIcon = 'reply'; replyIcon = 'reply';
replyTitle = intl.formatMessage(messages.reply); replyTitle = intl.formatMessage(messages.reply);
@ -319,6 +308,19 @@ class StatusActionBar extends ImmutablePureComponent {
replyTitle = intl.formatMessage(messages.replyAll); replyTitle = intl.formatMessage(messages.replyAll);
} }
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
let reblogTitle = '';
if (status.get('reblogged')) {
reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
} else if (publicStatus) {
reblogTitle = intl.formatMessage(messages.reblog);
} else if (reblogPrivate) {
reblogTitle = intl.formatMessage(messages.reblog_private);
} else {
reblogTitle = intl.formatMessage(messages.cannot_reblog);
}
const shareButton = ('share' in navigator) && publicStatus && ( const shareButton = ('share' in navigator) && publicStatus && (
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShareClick} /> <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShareClick} />
); );
@ -326,7 +328,7 @@ class StatusActionBar extends ImmutablePureComponent {
return ( return (
<div className='status__action-bar'> <div className='status__action-bar'>
<div className='status__action-bar__counter'><IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div> <div className='status__action-bar__counter'><IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div>
<IconButton className='status__action-bar-button' disabled={!publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /> <IconButton className='status__action-bar-button' disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} />
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /> <IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
{shareButton} {shareButton}

View File

@ -0,0 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
const TimelineHint = ({ resource, url }) => (
<div className='timeline-hint'>
<strong><FormattedMessage id='timeline_hint.remote_resource_not_displayed' defaultMessage='{resource} from other servers are not displayed.' values={{ resource }} /></strong>
<br />
<a href={url} target='_blank'><FormattedMessage id='account.browse_more_on_origin_server' defaultMessage='Browse more on the original profile' /></a>
</div>
);
TimelineHint.propTypes = {
resource: PropTypes.node.isRequired,
url: PropTypes.string.isRequired,
};
export default TimelineHint;

View File

@ -0,0 +1,170 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Textarea from 'react-textarea-autosize';
import { is } from 'immutable';
const messages = defineMessages({
placeholder: { id: 'account_note.placeholder', defaultMessage: 'Click to add a note' },
});
class InlineAlert extends React.PureComponent {
static propTypes = {
show: PropTypes.bool,
};
state = {
mountMessage: false,
};
static TRANSITION_DELAY = 200;
componentWillReceiveProps (nextProps) {
if (!this.props.show && nextProps.show) {
this.setState({ mountMessage: true });
} else if (this.props.show && !nextProps.show) {
setTimeout(() => this.setState({ mountMessage: false }), InlineAlert.TRANSITION_DELAY);
}
}
render () {
const { show } = this.props;
const { mountMessage } = this.state;
return (
<span aria-live='polite' role='status' className='inline-alert' style={{ opacity: show ? 1 : 0 }}>
{mountMessage && <FormattedMessage id='generic.saved' defaultMessage='Saved' />}
</span>
);
}
}
export default @injectIntl
class AccountNote extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
value: PropTypes.string,
onSave: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
state = {
value: null,
saving: false,
saved: false,
};
componentWillMount () {
this._reset();
}
componentWillReceiveProps (nextProps) {
const accountWillChange = !is(this.props.account, nextProps.account);
const newState = {};
if (accountWillChange && this._isDirty()) {
this._save(false);
}
if (accountWillChange || nextProps.value === this.state.value) {
newState.saving = false;
}
if (this.props.value !== nextProps.value) {
newState.value = nextProps.value;
}
this.setState(newState);
}
componentWillUnmount () {
if (this._isDirty()) {
this._save(false);
}
}
setTextareaRef = c => {
this.textarea = c;
}
handleChange = e => {
this.setState({ value: e.target.value, saving: false });
};
handleKeyDown = e => {
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
this._save();
if (this.textarea) {
this.textarea.blur();
}
} else if (e.keyCode === 27) {
e.preventDefault();
this._reset(() => {
if (this.textarea) {
this.textarea.blur();
}
});
}
}
handleBlur = () => {
if (this._isDirty()) {
this._save();
}
}
_save (showMessage = true) {
this.setState({ saving: true }, () => this.props.onSave(this.state.value));
if (showMessage) {
this.setState({ saved: true }, () => setTimeout(() => this.setState({ saved: false }), 2000));
}
}
_reset (callback) {
this.setState({ value: this.props.value }, callback);
}
_isDirty () {
return !this.state.saving && this.props.value !== null && this.state.value !== null && this.state.value !== this.props.value;
}
render () {
const { account, intl } = this.props;
const { value, saved } = this.state;
if (!account) {
return null;
}
return (
<div className='account__header__account-note'>
<label htmlFor={`account-note-${account.get('id')}`}>
<FormattedMessage id='account.account_note_header' defaultMessage='Note' /> <InlineAlert show={saved} />
</label>
<Textarea
id={`account-note-${account.get('id')}`}
className='account__header__account-note__content'
disabled={this.props.value === null || value === null}
placeholder={intl.formatMessage(messages.placeholder)}
value={value || ''}
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
onBlur={this.handleBlur}
ref={this.setTextareaRef}
/>
</div>
);
}
}

View File

@ -8,9 +8,11 @@ import { autoPlayGif, me, isStaff } from 'mastodon/initial_state';
import classNames from 'classnames'; import classNames from 'classnames';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import Avatar from 'mastodon/components/avatar'; import Avatar from 'mastodon/components/avatar';
import { shortNumberFormat } from 'mastodon/utils/numbers'; import { counterRenderer } from 'mastodon/components/common_counter';
import ShortNumber from 'mastodon/components/short_number';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container'; import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import AccountNoteContainer from '../containers/account_note_container';
const messages = defineMessages({ const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
@ -312,20 +314,31 @@ class Header extends ImmutablePureComponent {
</div> </div>
)} )}
{account.get('id') !== me && <AccountNoteContainer account={account} />}
{account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content' dangerouslySetInnerHTML={content} />} {account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content' dangerouslySetInnerHTML={content} />}
</div> </div>
<div className='account__header__extra__links'> <div className='account__header__extra__links'>
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/accounts/${account.get('id')}`} title={intl.formatNumber(account.get('statuses_count'))}> <NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/accounts/${account.get('id')}`} title={intl.formatNumber(account.get('statuses_count'))}>
<strong>{shortNumberFormat(account.get('statuses_count'))}</strong> <FormattedMessage id='account.posts' defaultMessage='Toots' /> <ShortNumber
value={account.get('statuses_count')}
renderer={counterRenderer('statuses')}
/>
</NavLink> </NavLink>
<NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/following`} title={intl.formatNumber(account.get('following_count'))}> <NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/following`} title={intl.formatNumber(account.get('following_count'))}>
<strong>{shortNumberFormat(account.get('following_count'))}</strong> <FormattedMessage id='account.follows' defaultMessage='Follows' /> <ShortNumber
value={account.get('following_count')}
renderer={counterRenderer('following')}
/>
</NavLink> </NavLink>
<NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/followers`} title={intl.formatNumber(account.get('followers_count'))}> <NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/followers`} title={intl.formatNumber(account.get('followers_count'))}>
<strong>{shortNumberFormat(account.get('followers_count'))}</strong> <FormattedMessage id='account.followers' defaultMessage='Followers' /> <ShortNumber
value={account.get('followers_count')}
renderer={counterRenderer('followers')}
/>
</NavLink> </NavLink>
</div> </div>
</div> </div>

View File

@ -0,0 +1,17 @@
import { connect } from 'react-redux';
import { submitAccountNote } from 'mastodon/actions/account_notes';
import AccountNote from '../components/account_note';
const mapStateToProps = (state, { account }) => ({
value: account.getIn(['relationship', 'note']),
});
const mapDispatchToProps = (dispatch, { account }) => ({
onSave (value) {
dispatch(submitAccountNote(account.get('id'), value));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(AccountNote);

View File

@ -23,6 +23,7 @@ export default class Header extends ImmutablePureComponent {
onUnblockDomain: PropTypes.func.isRequired, onUnblockDomain: PropTypes.func.isRequired,
onEndorseToggle: PropTypes.func.isRequired, onEndorseToggle: PropTypes.func.isRequired,
onAddToList: PropTypes.func.isRequired, onAddToList: PropTypes.func.isRequired,
onEditAccountNote: PropTypes.func.isRequired,
hideTabs: PropTypes.bool, hideTabs: PropTypes.bool,
domain: PropTypes.string.isRequired, domain: PropTypes.string.isRequired,
}; };
@ -83,6 +84,10 @@ export default class Header extends ImmutablePureComponent {
this.props.onAddToList(this.props.account); this.props.onAddToList(this.props.account);
} }
handleEditAccountNote = () => {
this.props.onEditAccountNote(this.props.account);
}
render () { render () {
const { account, hideTabs, identity_proofs } = this.props; const { account, hideTabs, identity_proofs } = this.props;
@ -108,6 +113,7 @@ export default class Header extends ImmutablePureComponent {
onUnblockDomain={this.handleUnblockDomain} onUnblockDomain={this.handleUnblockDomain}
onEndorseToggle={this.handleEndorseToggle} onEndorseToggle={this.handleEndorseToggle}
onAddToList={this.handleAddToList} onAddToList={this.handleAddToList}
onEditAccountNote={this.handleEditAccountNote}
domain={this.props.domain} domain={this.props.domain}
/> />

View File

@ -14,6 +14,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { fetchAccountIdentityProofs } from '../../actions/identity_proofs'; import { fetchAccountIdentityProofs } from '../../actions/identity_proofs';
import MissingIndicator from 'mastodon/components/missing_indicator'; import MissingIndicator from 'mastodon/components/missing_indicator';
import TimelineHint from 'mastodon/components/timeline_hint';
const emptyList = ImmutableList(); const emptyList = ImmutableList();
@ -21,6 +22,8 @@ const mapStateToProps = (state, { params: { accountId }, withReplies = false })
const path = withReplies ? `${accountId}:with_replies` : accountId; const path = withReplies ? `${accountId}:with_replies` : accountId;
return { return {
remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])),
remoteUrl: state.getIn(['accounts', accountId, 'url']),
isAccount: !!state.getIn(['accounts', accountId]), isAccount: !!state.getIn(['accounts', accountId]),
statusIds: state.getIn(['timelines', `account:${path}`, 'items'], emptyList), statusIds: state.getIn(['timelines', `account:${path}`, 'items'], emptyList),
featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], emptyList), featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], emptyList),
@ -30,6 +33,14 @@ const mapStateToProps = (state, { params: { accountId }, withReplies = false })
}; };
}; };
const RemoteHint = ({ url }) => (
<TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.statuses' defaultMessage='Older toots' />} />
);
RemoteHint.propTypes = {
url: PropTypes.string.isRequired,
};
export default @connect(mapStateToProps) export default @connect(mapStateToProps)
class AccountTimeline extends ImmutablePureComponent { class AccountTimeline extends ImmutablePureComponent {
@ -44,6 +55,8 @@ class AccountTimeline extends ImmutablePureComponent {
withReplies: PropTypes.bool, withReplies: PropTypes.bool,
blockedBy: PropTypes.bool, blockedBy: PropTypes.bool,
isAccount: PropTypes.bool, isAccount: PropTypes.bool,
remote: PropTypes.bool,
remoteUrl: PropTypes.string,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
}; };
@ -78,7 +91,7 @@ class AccountTimeline extends ImmutablePureComponent {
} }
render () { render () {
const { shouldUpdateScroll, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, isAccount, multiColumn } = this.props; const { shouldUpdateScroll, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, isAccount, multiColumn, remote, remoteUrl } = this.props;
if (!isAccount) { if (!isAccount) {
return ( return (
@ -97,7 +110,17 @@ class AccountTimeline extends ImmutablePureComponent {
); );
} }
const emptyMessage = blockedBy ? <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' /> : <FormattedMessage id='empty_column.account_timeline' defaultMessage='No toots here!' />; let emptyMessage;
if (blockedBy) {
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
} else if (remote && statusIds.isEmpty()) {
emptyMessage = <RemoteHint url={remoteUrl} />;
} else {
emptyMessage = <FormattedMessage id='empty_column.account_timeline' defaultMessage='No toots here!' />;
}
const remoteMessage = remote ? <RemoteHint url={remoteUrl} /> : null;
return ( return (
<Column> <Column>
@ -106,6 +129,7 @@ class AccountTimeline extends ImmutablePureComponent {
<StatusList <StatusList
prepend={<HeaderContainer accountId={this.props.params.accountId} />} prepend={<HeaderContainer accountId={this.props.params.accountId} />}
alwaysPrepend alwaysPrepend
append={remoteMessage}
scrollKey='account_timeline' scrollKey='account_timeline'
statusIds={blockedBy ? emptyList : statusIds} statusIds={blockedBy ? emptyList : statusIds}
featuredStatusIds={featuredStatusIds} featuredStatusIds={featuredStatusIds}

View File

@ -1,11 +1,17 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import WaveSurfer from 'wavesurfer.js';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import { formatTime } from 'mastodon/features/video'; import { formatTime } from 'mastodon/features/video';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import classNames from 'classnames'; import classNames from 'classnames';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import { getPointerPosition, fileNameFromURL } from 'mastodon/features/video';
import { debounce } from 'lodash';
const hex2rgba = (hex, alpha = 1) => {
const [r, g, b] = hex.match(/\w\w/g).map(x => parseInt(x, 16));
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
};
const messages = defineMessages({ const messages = defineMessages({
play: { id: 'video.play', defaultMessage: 'Play' }, play: { id: 'video.play', defaultMessage: 'Play' },
@ -15,131 +21,151 @@ const messages = defineMessages({
download: { id: 'video.download', defaultMessage: 'Download file' }, download: { id: 'video.download', defaultMessage: 'Download file' },
}); });
const TICK_SIZE = 10;
const PADDING = 180;
export default @injectIntl export default @injectIntl
class Audio extends React.PureComponent { class Audio extends React.PureComponent {
static propTypes = { static propTypes = {
src: PropTypes.string.isRequired, src: PropTypes.string.isRequired,
alt: PropTypes.string, alt: PropTypes.string,
poster: PropTypes.string,
duration: PropTypes.number, duration: PropTypes.number,
peaks: PropTypes.arrayOf(PropTypes.number), width: PropTypes.number,
height: PropTypes.number, height: PropTypes.number,
preload: PropTypes.bool,
editable: PropTypes.bool, editable: PropTypes.bool,
fullscreen: PropTypes.bool,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
cacheWidth: PropTypes.func,
backgroundColor: PropTypes.string,
foregroundColor: PropTypes.string,
accentColor: PropTypes.string,
}; };
state = { state = {
width: this.props.width,
currentTime: 0, currentTime: 0,
buffer: 0,
duration: null, duration: null,
paused: true, paused: true,
muted: false, muted: false,
volume: 0.5, volume: 0.5,
dragging: false,
}; };
// Hard coded in components.scss setPlayerRef = c => {
// Any way to get ::before values programatically? this.player = c;
volWidth = 50;
volOffset = 70;
volHandleOffset = v => { if (this.player) {
const offset = v * this.volWidth + this.volOffset; this._setDimensions();
}
}
return (offset > 110) ? 110 : offset; _setDimensions () {
const width = this.player.offsetWidth;
const height = this.props.fullscreen ? this.player.offsetHeight : (width / (16/9));
if (this.props.cacheWidth) {
this.props.cacheWidth(width);
}
this.setState({ width, height });
}
setSeekRef = c => {
this.seek = c;
} }
setVolumeRef = c => { setVolumeRef = c => {
this.volume = c; this.volume = c;
} }
setWaveformRef = c => { setAudioRef = c => {
this.waveform = c; this.audio = c;
if (this.audio) {
this.setState({ volume: this.audio.volume, muted: this.audio.muted });
}
}
setCanvasRef = c => {
this.canvas = c;
if (c) {
this.canvasContext = c.getContext('2d');
}
} }
componentDidMount () { componentDidMount () {
if (this.waveform) {
this._updateWaveform();
}
window.addEventListener('scroll', this.handleScroll); window.addEventListener('scroll', this.handleScroll);
window.addEventListener('resize', this.handleResize, { passive: true });
} }
componentDidUpdate (prevProps) { componentDidUpdate (prevProps, prevState) {
if (this.waveform && prevProps.src !== this.props.src) { if (prevProps.src !== this.props.src || this.state.width !== prevState.width || this.state.height !== prevState.height || prevProps.accentColor !== this.props.accentColor) {
this._updateWaveform(); this._clear();
this._draw();
} }
} }
componentWillUnmount () { componentWillUnmount () {
window.removeEventListener('scroll', this.handleScroll); window.removeEventListener('scroll', this.handleScroll);
window.removeEventListener('resize', this.handleResize);
if (this.wavesurfer) {
this.wavesurfer.destroy();
this.wavesurfer = null;
}
}
_updateWaveform () {
const { src, height, duration, peaks, preload } = this.props;
const progressColor = window.getComputedStyle(document.querySelector('.audio-player__progress-placeholder')).getPropertyValue('background-color');
const waveColor = window.getComputedStyle(document.querySelector('.audio-player__wave-placeholder')).getPropertyValue('background-color');
if (this.wavesurfer) {
this.wavesurfer.destroy();
this.loaded = false;
}
const wavesurfer = WaveSurfer.create({
container: this.waveform,
height,
barWidth: 3,
cursorWidth: 0,
progressColor,
waveColor,
backend: 'MediaElement',
interact: preload,
});
wavesurfer.setVolume(this.state.volume);
if (preload) {
wavesurfer.load(src);
this.loaded = true;
} else {
wavesurfer.load(src, peaks, 'none', duration);
this.loaded = false;
}
wavesurfer.on('ready', () => this.setState({ duration: Math.floor(wavesurfer.getDuration()) }));
wavesurfer.on('audioprocess', () => this.setState({ currentTime: Math.floor(wavesurfer.getCurrentTime()) }));
wavesurfer.on('pause', () => this.setState({ paused: true }));
wavesurfer.on('play', () => this.setState({ paused: false }));
wavesurfer.on('volume', volume => this.setState({ volume }));
wavesurfer.on('mute', muted => this.setState({ muted }));
this.wavesurfer = wavesurfer;
} }
togglePlay = () => { togglePlay = () => {
if (this.state.paused) { if (this.state.paused) {
if (!this.props.preload && !this.loaded) { this.setState({ paused: false }, () => this.audio.play());
this.wavesurfer.createBackend();
this.wavesurfer.createPeakCache();
this.wavesurfer.load(this.props.src);
this.wavesurfer.toggleInteraction();
this.loaded = true;
}
this.setState({ paused: false }, () => this.wavesurfer.play());
} else { } else {
this.setState({ paused: true }, () => this.wavesurfer.pause()); this.setState({ paused: true }, () => this.audio.pause());
}
}
handleResize = debounce(() => {
if (this.player) {
this._setDimensions();
}
}, 250, {
trailing: true,
});
handlePlay = () => {
this.setState({ paused: false });
if (this.canvas && !this.audioContext) {
this._initAudioContext();
}
if (this.audioContext && this.audioContext.state === 'suspended') {
this.audioContext.resume();
}
this._renderCanvas();
}
handlePause = () => {
this.setState({ paused: true });
if (this.audioContext) {
this.audioContext.suspend();
}
}
handleProgress = () => {
const lastTimeRange = this.audio.buffered.length - 1;
if (lastTimeRange > -1) {
this.setState({ buffer: Math.ceil(this.audio.buffered.end(lastTimeRange) / this.audio.duration * 100) });
} }
} }
toggleMute = () => { toggleMute = () => {
const muted = !this.state.muted; const muted = !this.state.muted;
this.setState({ muted }, () => this.wavesurfer.setMute(muted));
this.setState({ muted }, () => {
this.audio.muted = muted;
});
} }
handleVolumeMouseDown = e => { handleVolumeMouseDown = e => {
@ -161,86 +187,361 @@ class Audio extends React.PureComponent {
document.removeEventListener('touchend', this.handleVolumeMouseUp, true); document.removeEventListener('touchend', this.handleVolumeMouseUp, true);
} }
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.audio.pause();
this.handleMouseMove(e);
e.preventDefault();
e.stopPropagation();
}
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.audio.play();
}
handleMouseMove = throttle(e => {
const { x } = getPointerPosition(this.seek, e);
const currentTime = this.audio.duration * x;
if (!isNaN(currentTime)) {
this.setState({ currentTime }, () => {
this.audio.currentTime = currentTime;
});
}
}, 15);
handleTimeUpdate = () => {
this.setState({
currentTime: this.audio.currentTime,
duration: Math.floor(this.audio.duration),
});
}
handleMouseVolSlide = throttle(e => { handleMouseVolSlide = throttle(e => {
const rect = this.volume.getBoundingClientRect(); const { x } = getPointerPosition(this.volume, e);
const x = (e.clientX - rect.left) / this.volWidth; // x position within the element.
if(!isNaN(x)) { if(!isNaN(x)) {
let slideamt = x; this.setState({ volume: x }, () => {
this.audio.volume = x;
if (x > 1) { });
slideamt = 1;
} else if(x < 0) {
slideamt = 0;
}
this.wavesurfer.setVolume(slideamt);
} }
}, 60); }, 15);
handleScroll = throttle(() => { handleScroll = throttle(() => {
if (!this.waveform || !this.wavesurfer) { if (!this.canvas || !this.audio) {
return; return;
} }
const { top, height } = this.waveform.getBoundingClientRect(); const { top, height } = this.canvas.getBoundingClientRect();
const inView = (top <= (window.innerHeight || document.documentElement.clientHeight)) && (top + height >= 0); const inView = (top <= (window.innerHeight || document.documentElement.clientHeight)) && (top + height >= 0);
if (!this.state.paused && !inView) { if (!this.state.paused && !inView) {
this.setState({ paused: true }, () => this.wavesurfer.pause()); this.setState({ paused: true }, () => this.audio.pause());
} }
}, 150, { trailing: true }) }, 150, { trailing: true });
handleMouseEnter = () => {
this.setState({ hovered: true });
}
handleMouseLeave = () => {
this.setState({ hovered: false });
}
_initAudioContext () {
const context = new AudioContext();
const analyser = context.createAnalyser();
const source = context.createMediaElementSource(this.audio);
analyser.smoothingTimeConstant = 0.6;
analyser.fftSize = 2048;
source.connect(analyser);
source.connect(context.destination);
this.audioContext = context;
this.analyser = analyser;
}
handleDownload = () => {
fetch(this.props.src).then(res => res.blob()).then(blob => {
const element = document.createElement('a');
const objectURL = URL.createObjectURL(blob);
element.setAttribute('href', objectURL);
element.setAttribute('download', fileNameFromURL(this.props.src));
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
URL.revokeObjectURL(objectURL);
}).catch(err => {
console.error(err);
});
}
_renderCanvas () {
requestAnimationFrame(() => {
this.handleTimeUpdate();
this._clear();
this._draw();
if (!this.state.paused) {
this._renderCanvas();
}
});
}
_clear () {
this.canvasContext.clearRect(0, 0, this.state.width, this.state.height);
}
_draw () {
this.canvasContext.save();
const ticks = this._getTicks(360 * this._getScaleCoefficient(), TICK_SIZE);
ticks.forEach(tick => {
this._drawTick(tick.x1, tick.y1, tick.x2, tick.y2);
});
this.canvasContext.restore();
}
_getRadius () {
return parseInt(((this.state.height || this.props.height) - (PADDING * this._getScaleCoefficient()) * 2) / 2);
}
_getScaleCoefficient () {
return (this.state.height || this.props.height) / 982;
}
_getTicks (count, size, animationParams = [0, 90]) {
const radius = this._getRadius();
const ticks = this._getTickPoints(count);
const lesser = 200;
const m = [];
const bufferLength = this.analyser ? this.analyser.frequencyBinCount : 0;
const frequencyData = new Uint8Array(bufferLength);
const allScales = [];
const scaleCoefficient = this._getScaleCoefficient();
if (this.analyser) {
this.analyser.getByteFrequencyData(frequencyData);
}
ticks.forEach((tick, i) => {
const coef = 1 - i / (ticks.length * 2.5);
let delta = ((frequencyData[i] || 0) - lesser * coef) * scaleCoefficient;
if (delta < 0) {
delta = 0;
}
let k;
if (animationParams[0] <= tick.angle && tick.angle <= animationParams[1]) {
k = radius / (radius - this._getSize(tick.angle, animationParams[0], animationParams[1]) - delta);
} else {
k = radius / (radius - (size + delta));
}
const x1 = tick.x * (radius - size);
const y1 = tick.y * (radius - size);
const x2 = x1 * k;
const y2 = y1 * k;
m.push({ x1, y1, x2, y2 });
if (i < 20) {
let scale = delta / (200 * scaleCoefficient);
scale = scale < 1 ? 1 : scale;
allScales.push(scale);
}
});
const scale = allScales.reduce((pv, cv) => pv + cv, 0) / allScales.length;
return m.map(({ x1, y1, x2, y2 }) => ({
x1: x1,
y1: y1,
x2: x2 * scale,
y2: y2 * scale,
}));
}
_getSize (angle, l, r) {
const scaleCoefficient = this._getScaleCoefficient();
const maxTickSize = TICK_SIZE * 9 * scaleCoefficient;
const m = (r - l) / 2;
const x = (angle - l);
let h;
if (x === m) {
return maxTickSize;
}
const d = Math.abs(m - x);
const v = 40 * Math.sqrt(1 / d);
if (v > maxTickSize) {
h = maxTickSize;
} else {
h = Math.max(TICK_SIZE, v);
}
return h;
}
_getTickPoints (count) {
const PI = 360;
const coords = [];
const step = PI / count;
let rad;
for(let deg = 0; deg < PI; deg += step) {
rad = deg * Math.PI / (PI / 2);
coords.push({ x: Math.cos(rad), y: -Math.sin(rad), angle: deg });
}
return coords;
}
_drawTick (x1, y1, x2, y2) {
const cx = this._getCX();
const cy = this._getCY();
const dx1 = Math.ceil(cx + x1);
const dy1 = Math.ceil(cy + y1);
const dx2 = Math.ceil(cx + x2);
const dy2 = Math.ceil(cy + y2);
const gradient = this.canvasContext.createLinearGradient(dx1, dy1, dx2, dy2);
const mainColor = this._getAccentColor();
const lastColor = hex2rgba(mainColor, 0);
gradient.addColorStop(0, mainColor);
gradient.addColorStop(0.6, mainColor);
gradient.addColorStop(1, lastColor);
this.canvasContext.beginPath();
this.canvasContext.strokeStyle = gradient;
this.canvasContext.lineWidth = 2;
this.canvasContext.moveTo(dx1, dy1);
this.canvasContext.lineTo(dx2, dy2);
this.canvasContext.stroke();
}
_getCX() {
return Math.floor(this.state.width / 2);
}
_getCY() {
return Math.floor(this._getRadius() + (PADDING * this._getScaleCoefficient()));
}
_getAccentColor () {
return this.props.accentColor || '#ffffff';
}
_getBackgroundColor () {
return this.props.backgroundColor || '#000000';
}
_getForegroundColor () {
return this.props.foregroundColor || '#ffffff';
}
render () { render () {
const { height, intl, alt, editable } = this.props; const { src, intl, alt, editable } = this.props;
const { paused, muted, volume, currentTime } = this.state; const { paused, muted, volume, currentTime, duration, buffer, dragging } = this.state;
const progress = (currentTime / duration) * 100;
const volumeWidth = muted ? 0 : volume * this.volWidth;
const volumeHandleLoc = muted ? this.volHandleOffset(0) : this.volHandleOffset(volume);
return ( return (
<div className={classNames('audio-player', { editable })}> <div className={classNames('audio-player', { editable })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), width: '100%', height: this.props.fullscreen ? '100%' : (this.state.height || this.props.height) }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<div className='audio-player__progress-placeholder' style={{ display: 'none' }} /> <audio
<div className='audio-player__wave-placeholder' style={{ display: 'none' }} /> src={src}
ref={this.setAudioRef}
<div preload='none'
className='audio-player__waveform' onPlay={this.handlePlay}
aria-label={alt} onPause={this.handlePause}
title={alt} onProgress={this.handleProgress}
style={{ height }} crossOrigin='anonymous'
ref={this.setWaveformRef}
/> />
<canvas
role='button'
className='audio-player__canvas'
width={this.state.width}
height={this.state.height}
style={{ width: '100%', position: 'absolute', top: 0, left: 0 }}
ref={this.setCanvasRef}
onClick={this.togglePlay}
title={alt}
aria-label={alt}
/>
<img
src={this.props.poster}
alt=''
width={(this._getRadius() - TICK_SIZE) * 2}
height={(this._getRadius() - TICK_SIZE) * 2}
style={{ position: 'absolute', left: this._getCX(), top: this._getCY(), transform: 'translate(-50%, -50%)', borderRadius: '50%', pointerEvents: 'none' }}
/>
<div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}>
<div className='video-player__seek__buffer' style={{ width: `${buffer}%` }} />
<div className='video-player__seek__progress' style={{ width: `${progress}%`, backgroundColor: this._getAccentColor() }} />
<span
className={classNames('video-player__seek__handle', { active: dragging })}
tabIndex='0'
style={{ left: `${progress}%`, backgroundColor: this._getAccentColor() }}
/>
</div>
<div className='video-player__controls active'> <div className='video-player__controls active'>
<div className='video-player__buttons-bar'> <div className='video-player__buttons-bar'>
<div className='video-player__buttons left'> <div className='video-player__buttons left'>
<button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button> <button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
<button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button> <button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
<div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}> <div className={classNames('video-player__volume', { active: this.state.hovered })} ref={this.setVolumeRef} onMouseDown={this.handleVolumeMouseDown}>
&nbsp; <div className='video-player__volume__current' style={{ width: `${volume * 100}%`, backgroundColor: this._getAccentColor() }} />
<div className='video-player__volume__current' style={{ width: `${volumeWidth}px` }} />
<span <span
className={classNames('video-player__volume__handle')} className={classNames('video-player__volume__handle')}
tabIndex='0' tabIndex='0'
style={{ left: `${volumeHandleLoc}px` }} style={{ left: `${volume * 100}%`, backgroundColor: this._getAccentColor() }}
/> />
</div> </div>
<span> <span className='video-player__time'>
<span className='video-player__time-current'>{formatTime(currentTime)}</span> <span className='video-player__time-current'>{formatTime(Math.floor(currentTime))}</span>
<span className='video-player__time-sep'>/</span> <span className='video-player__time-sep'>/</span>
<span className='video-player__time-total'>{formatTime(this.state.duration || Math.floor(this.props.duration))}</span> <span className='video-player__time-total'>{formatTime(this.state.duration || Math.floor(this.props.duration))}</span>
</span> </span>
</div> </div>
<div className='video-player__buttons right'> <div className='video-player__buttons right'>
<button type='button' title={intl.formatMessage(messages.download)} aria-label={intl.formatMessage(messages.download)}> <button type='button' title={intl.formatMessage(messages.download)} aria-label={intl.formatMessage(messages.download)} onClick={this.handleDownload}><Icon id='download' fixedWidth /></button>
<a className='video-player__download__icon' href={this.props.src} download>
<Icon id={'download'} fixedWidth />
</a>
</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -203,7 +203,7 @@ class EmojiPickerMenu extends React.PureComponent {
if (!emoji.native) { if (!emoji.native) {
emoji.native = emoji.colons; emoji.native = emoji.colons;
} }
if (!event.ctrlKey) { if (!(event.ctrlKey || event.metaKey)) {
this.props.onClose(); this.props.onClose();
} }
this.props.onPick(emoji); this.props.onPick(emoji);

View File

@ -7,11 +7,9 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
const messages = defineMessages({ const messages = defineMessages({
upload: { id: 'upload_button.label', defaultMessage: 'Add media ({formats})' }, upload: { id: 'upload_button.label', defaultMessage: 'Add images, a video or an audio file' },
}); });
const SUPPORTED_FORMATS = 'JPEG, PNG, GIF, WebM, MP4, MOV, OGG, WAV, MP3, FLAC';
const makeMapStateToProps = () => { const makeMapStateToProps = () => {
const mapStateToProps = state => ({ const mapStateToProps = state => ({
acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']), acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']),
@ -60,11 +58,13 @@ class UploadButton extends ImmutablePureComponent {
return null; return null;
} }
const message = intl.formatMessage(messages.upload);
return ( return (
<div className='compose-form__upload-button'> <div className='compose-form__upload-button'>
<IconButton icon='paperclip' title={intl.formatMessage(messages.upload, { formats: SUPPORTED_FORMATS })} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} /> <IconButton icon='paperclip' title={message} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} />
<label> <label>
<span style={{ display: 'none' }}>{intl.formatMessage(messages.upload, { formats: SUPPORTED_FORMATS })}</span> <span style={{ display: 'none' }}>{message}</span>
<input <input
key={resetFileKey} key={resetFileKey}
ref={this.setRef} ref={this.setRef}

View File

@ -11,8 +11,14 @@ import RelativeTimestamp from 'mastodon/components/relative_timestamp';
import IconButton from 'mastodon/components/icon_button'; import IconButton from 'mastodon/components/icon_button';
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
import { autoPlayGif, me, unfollowModal } from 'mastodon/initial_state'; import { autoPlayGif, me, unfollowModal } from 'mastodon/initial_state';
import { shortNumberFormat } from 'mastodon/utils/numbers'; import ShortNumber from 'mastodon/components/short_number';
import { followAccount, unfollowAccount, blockAccount, unblockAccount, unmuteAccount } from 'mastodon/actions/accounts'; import {
followAccount,
unfollowAccount,
blockAccount,
unblockAccount,
unmuteAccount,
} from 'mastodon/actions/accounts';
import { openModal } from 'mastodon/actions/modal'; import { openModal } from 'mastodon/actions/modal';
import { initMuteModal } from 'mastodon/actions/mutes'; import { initMuteModal } from 'mastodon/actions/mutes';
@ -22,7 +28,10 @@ const messages = defineMessages({
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, unfollowConfirm: {
id: 'confirmations.unfollow.confirm',
defaultMessage: 'Unfollow',
},
}); });
const makeMapStateToProps = () => { const makeMapStateToProps = () => {
@ -36,15 +45,25 @@ const makeMapStateToProps = () => {
}; };
const mapDispatchToProps = (dispatch, { intl }) => ({ const mapDispatchToProps = (dispatch, { intl }) => ({
onFollow(account) {
onFollow (account) { if (
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) { account.getIn(['relationship', 'following']) ||
account.getIn(['relationship', 'requested'])
) {
if (unfollowModal) { if (unfollowModal) {
dispatch(openModal('CONFIRM', { dispatch(
message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, openModal('CONFIRM', {
confirm: intl.formatMessage(messages.unfollowConfirm), message: (
onConfirm: () => dispatch(unfollowAccount(account.get('id'))), <FormattedMessage
})); id='confirmations.unfollow.message'
defaultMessage='Are you sure you want to unfollow {name}?'
values={{ name: <strong>@{account.get('acct')}</strong> }}
/>
),
confirm: intl.formatMessage(messages.unfollowConfirm),
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
}),
);
} else { } else {
dispatch(unfollowAccount(account.get('id'))); dispatch(unfollowAccount(account.get('id')));
} }
@ -53,7 +72,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
} }
}, },
onBlock (account) { onBlock(account) {
if (account.getIn(['relationship', 'blocking'])) { if (account.getIn(['relationship', 'blocking'])) {
dispatch(unblockAccount(account.get('id'))); dispatch(unblockAccount(account.get('id')));
} else { } else {
@ -61,17 +80,17 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
} }
}, },
onMute (account) { onMute(account) {
if (account.getIn(['relationship', 'muting'])) { if (account.getIn(['relationship', 'muting'])) {
dispatch(unmuteAccount(account.get('id'))); dispatch(unmuteAccount(account.get('id')));
} else { } else {
dispatch(initMuteModal(account)); dispatch(initMuteModal(account));
} }
}, },
}); });
export default @injectIntl export default
@injectIntl
@connect(makeMapStateToProps, mapDispatchToProps) @connect(makeMapStateToProps, mapDispatchToProps)
class AccountCard extends ImmutablePureComponent { class AccountCard extends ImmutablePureComponent {
@ -83,7 +102,7 @@ class AccountCard extends ImmutablePureComponent {
onMute: PropTypes.func.isRequired, onMute: PropTypes.func.isRequired,
}; };
_updateEmojis () { _updateEmojis() {
const node = this.node; const node = this.node;
if (!node || autoPlayGif) { if (!node || autoPlayGif) {
@ -104,68 +123,113 @@ class AccountCard extends ImmutablePureComponent {
} }
} }
componentDidMount () { componentDidMount() {
this._updateEmojis(); this._updateEmojis();
} }
componentDidUpdate () { componentDidUpdate() {
this._updateEmojis(); this._updateEmojis();
} }
handleEmojiMouseEnter = ({ target }) => { handleEmojiMouseEnter = ({ target }) => {
target.src = target.getAttribute('data-original'); target.src = target.getAttribute('data-original');
} };
handleEmojiMouseLeave = ({ target }) => { handleEmojiMouseLeave = ({ target }) => {
target.src = target.getAttribute('data-static'); target.src = target.getAttribute('data-static');
} };
handleFollow = () => { handleFollow = () => {
this.props.onFollow(this.props.account); this.props.onFollow(this.props.account);
} };
handleBlock = () => { handleBlock = () => {
this.props.onBlock(this.props.account); this.props.onBlock(this.props.account);
} };
handleMute = () => { handleMute = () => {
this.props.onMute(this.props.account); this.props.onMute(this.props.account);
} };
setRef = (c) => { setRef = (c) => {
this.node = c; this.node = c;
} };
render () { render() {
const { account, intl } = this.props; const { account, intl } = this.props;
let buttons; let buttons;
if (account.get('id') !== me && account.get('relationship', null) !== null) { if (
account.get('id') !== me &&
account.get('relationship', null) !== null
) {
const following = account.getIn(['relationship', 'following']); const following = account.getIn(['relationship', 'following']);
const requested = account.getIn(['relationship', 'requested']); const requested = account.getIn(['relationship', 'requested']);
const blocking = account.getIn(['relationship', 'blocking']); const blocking = account.getIn(['relationship', 'blocking']);
const muting = account.getIn(['relationship', 'muting']); const muting = account.getIn(['relationship', 'muting']);
if (requested) { if (requested) {
buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />; buttons = (
<IconButton
disabled
icon='hourglass'
title={intl.formatMessage(messages.requested)}
/>
);
} else if (blocking) { } else if (blocking) {
buttons = <IconButton active icon='unlock' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />; buttons = (
<IconButton
active
icon='unlock'
title={intl.formatMessage(messages.unblock, {
name: account.get('username'),
})}
onClick={this.handleBlock}
/>
);
} else if (muting) { } else if (muting) {
buttons = <IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />; buttons = (
<IconButton
active
icon='volume-up'
title={intl.formatMessage(messages.unmute, {
name: account.get('username'),
})}
onClick={this.handleMute}
/>
);
} else if (!account.get('moved') || following) { } else if (!account.get('moved') || following) {
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />; buttons = (
<IconButton
icon={following ? 'user-times' : 'user-plus'}
title={intl.formatMessage(
following ? messages.unfollow : messages.follow,
)}
onClick={this.handleFollow}
active={following}
/>
);
} }
} }
return ( return (
<div className='directory__card'> <div className='directory__card'>
<div className='directory__card__img'> <div className='directory__card__img'>
<img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' /> <img
src={
autoPlayGif ? account.get('header') : account.get('header_static')
}
alt=''
/>
</div> </div>
<div className='directory__card__bar'> <div className='directory__card__bar'>
<Permalink className='directory__card__bar__name' href={account.get('url')} to={`/accounts/${account.get('id')}`}> <Permalink
className='directory__card__bar__name'
href={account.get('url')}
to={`/accounts/${account.get('id')}`}
>
<Avatar account={account} size={48} /> <Avatar account={account} size={48} />
<DisplayName account={account} /> <DisplayName account={account} />
</Permalink> </Permalink>
@ -176,13 +240,44 @@ class AccountCard extends ImmutablePureComponent {
</div> </div>
<div className='directory__card__extra' ref={this.setRef}> <div className='directory__card__extra' ref={this.setRef}>
<div className='account__header__content' dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }} /> <div
className='account__header__content'
dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
/>
</div> </div>
<div className='directory__card__extra'> <div className='directory__card__extra'>
<div className='accounts-table__count'>{shortNumberFormat(account.get('statuses_count'))} <small><FormattedMessage id='account.posts' defaultMessage='Toots' /></small></div> <div className='accounts-table__count'>
<div className='accounts-table__count'>{shortNumberFormat(account.get('followers_count'))} <small><FormattedMessage id='account.followers' defaultMessage='Followers' /></small></div> <ShortNumber value={account.get('statuses_count')} />
<div className='accounts-table__count'>{account.get('last_status_at') === null ? <FormattedMessage id='account.never_active' defaultMessage='Never' /> : <RelativeTimestamp timestamp={account.get('last_status_at')} />} <small><FormattedMessage id='account.last_status' defaultMessage='Last active' /></small></div> <small>
<FormattedMessage id='account.posts' defaultMessage='Toots' />
</small>
</div>
<div className='accounts-table__count'>
<ShortNumber value={account.get('followers_count')} />{' '}
<small>
<FormattedMessage
id='account.followers'
defaultMessage='Followers'
/>
</small>
</div>
<div className='accounts-table__count'>
{account.get('last_status_at') === null ? (
<FormattedMessage
id='account.never_active'
defaultMessage='Never'
/>
) : (
<RelativeTimestamp timestamp={account.get('last_status_at')} />
)}{' '}
<small>
<FormattedMessage
id='account.last_status'
defaultMessage='Last active'
/>
</small>
</div>
</div> </div>
</div> </div>
); );

View File

@ -76,7 +76,17 @@ describe('emoji', () => {
it('skips the textual presentation VS15 character', () => { it('skips the textual presentation VS15 character', () => {
expect(emojify('✴︎')) // This is U+2734 EIGHT POINTED BLACK STAR then U+FE0E VARIATION SELECTOR-15 expect(emojify('✴︎')) // This is U+2734 EIGHT POINTED BLACK STAR then U+FE0E VARIATION SELECTOR-15
.toEqual('<img draggable="false" class="emojione" alt="✴" title=":eight_pointed_black_star:" src="/emoji/2734.svg" />'); .toEqual('<img draggable="false" class="emojione" alt="✴" title=":eight_pointed_black_star:" src="/emoji/2734_border.svg" />');
});
it('does an simple emoji properly', () => {
expect(emojify('♀♂'))
.toEqual('<img draggable="false" class="emojione" alt="♀" title=":female_sign:" src="/emoji/2640.svg" /><img draggable="false" class="emojione" alt="♂" title=":male_sign:" src="/emoji/2642.svg" />');
});
it('does an emoji containing ZWJ properly', () => {
expect(emojify('💂‍♀️💂‍♂️'))
.toEqual('<img draggable="false" class="emojione" alt="💂\u200D♀" title=":female-guard:" src="/emoji/1f482-200d-2640-fe0f_border.svg" /><img draggable="false" class="emojione" alt="💂\u200D♂" title=":male-guard:" src="/emoji/1f482-200d-2642-fe0f_border.svg" />');
}); });
}); });
}); });

View File

@ -6,6 +6,20 @@ const trie = new Trie(Object.keys(unicodeMapping));
const assetHost = process.env.CDN_HOST || ''; const assetHost = process.env.CDN_HOST || '';
// Convert to file names from emojis. (For different variation selector emojis)
const emojiFilenames = (emojis) => {
return emojis.map(v => unicodeMapping[v].filename);
};
// Emoji requiring extra borders depending on theme
const darkEmoji = emojiFilenames(['🎱', '🐜', '⚫', '🖤', '⬛', '◼️', '◾', '◼️', '✒️', '▪️', '💣', '🎳', '📷', '📸', '♣️', '🕶️', '✴️', '🔌', '💂‍♀️', '📽️', '🍳', '🦍', '💂', '🔪', '🕳️', '🕹️', '🕋', '🖊️', '🖋️', '💂‍♂️', '🎤', '🎓', '🎥', '🎼', '♠️', '🎩', '🦃', '📼', '📹', '🎮', '🐃', '🏴']);
const lightEmoji = emojiFilenames(['👽', '⚾', '🐔', '☁️', '💨', '🕊️', '👀', '🍥', '👻', '🐐', '❕', '❔', '⛸️', '🌩️', '🔊', '🔇', '📃', '🌧️', '🐏', '🍚', '🍙', '🐓', '🐑', '💀', '☠️', '🌨️', '🔉', '🔈', '💬', '💭', '🏐', '🏳️', '⚪', '⬜', '◽', '◻️', '▫️']);
const emojiFilename = (filename) => {
const borderedEmoji = (document.body && document.body.classList.contains('theme-mastodon-light')) ? lightEmoji : darkEmoji;
return borderedEmoji.includes(filename) ? (filename + '_border') : filename;
};
const emojify = (str, customEmojis = {}) => { const emojify = (str, customEmojis = {}) => {
const tagCharsWithoutEmojis = '<&'; const tagCharsWithoutEmojis = '<&';
const tagCharsWithEmojis = Object.keys(customEmojis).length ? '<&:' : '<&'; const tagCharsWithEmojis = Object.keys(customEmojis).length ? '<&:' : '<&';
@ -60,7 +74,7 @@ const emojify = (str, customEmojis = {}) => {
} else { // matched to unicode emoji } else { // matched to unicode emoji
const { filename, shortCode } = unicodeMapping[match]; const { filename, shortCode } = unicodeMapping[match];
const title = shortCode ? `:${shortCode}:` : ''; const title = shortCode ? `:${shortCode}:` : '';
replacement = `<img draggable="false" class="emojione" alt="${match}" title="${title}" src="${assetHost}/emoji/${filename}.svg" />`; replacement = `<img draggable="false" class="emojione" alt="${match}" title="${title}" src="${assetHost}/emoji/${emojiFilename(filename)}.svg" />`;
rend = i + match.length; rend = i + match.length;
// If the matched character was followed by VS15 (for selecting text presentation), skip it. // If the matched character was followed by VS15 (for selecting text presentation), skip it.
if (str.codePointAt(rend) === 65038) { if (str.codePointAt(rend) === 65038) {

View File

@ -17,8 +17,11 @@ import HeaderContainer from '../account_timeline/containers/header_container';
import ColumnBackButton from '../../components/column_back_button'; import ColumnBackButton from '../../components/column_back_button';
import ScrollableList from '../../components/scrollable_list'; import ScrollableList from '../../components/scrollable_list';
import MissingIndicator from 'mastodon/components/missing_indicator'; import MissingIndicator from 'mastodon/components/missing_indicator';
import TimelineHint from 'mastodon/components/timeline_hint';
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
remote: !!(state.getIn(['accounts', props.params.accountId, 'acct']) !== state.getIn(['accounts', props.params.accountId, 'username'])),
remoteUrl: state.getIn(['accounts', props.params.accountId, 'url']),
isAccount: !!state.getIn(['accounts', props.params.accountId]), isAccount: !!state.getIn(['accounts', props.params.accountId]),
accountIds: state.getIn(['user_lists', 'followers', props.params.accountId, 'items']), accountIds: state.getIn(['user_lists', 'followers', props.params.accountId, 'items']),
hasMore: !!state.getIn(['user_lists', 'followers', props.params.accountId, 'next']), hasMore: !!state.getIn(['user_lists', 'followers', props.params.accountId, 'next']),
@ -26,6 +29,14 @@ const mapStateToProps = (state, props) => ({
blockedBy: state.getIn(['relationships', props.params.accountId, 'blocked_by'], false), blockedBy: state.getIn(['relationships', props.params.accountId, 'blocked_by'], false),
}); });
const RemoteHint = ({ url }) => (
<TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.followers' defaultMessage='Followers' />} />
);
RemoteHint.propTypes = {
url: PropTypes.string.isRequired,
};
export default @connect(mapStateToProps) export default @connect(mapStateToProps)
class Followers extends ImmutablePureComponent { class Followers extends ImmutablePureComponent {
@ -38,6 +49,8 @@ class Followers extends ImmutablePureComponent {
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
blockedBy: PropTypes.bool, blockedBy: PropTypes.bool,
isAccount: PropTypes.bool, isAccount: PropTypes.bool,
remote: PropTypes.bool,
remoteUrl: PropTypes.string,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
}; };
@ -60,7 +73,7 @@ class Followers extends ImmutablePureComponent {
}, 300, { leading: true }); }, 300, { leading: true });
render () { render () {
const { shouldUpdateScroll, accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading } = this.props; const { shouldUpdateScroll, accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, remote, remoteUrl } = this.props;
if (!isAccount) { if (!isAccount) {
return ( return (
@ -78,7 +91,17 @@ class Followers extends ImmutablePureComponent {
); );
} }
const emptyMessage = blockedBy ? <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' /> : <FormattedMessage id='account.followers.empty' defaultMessage='No one follows this user yet.' />; let emptyMessage;
if (blockedBy) {
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
} else if (remote && accountIds.isEmpty()) {
emptyMessage = <RemoteHint url={remoteUrl} />;
} else {
emptyMessage = <FormattedMessage id='account.followers.empty' defaultMessage='No one follows this user yet.' />;
}
const remoteMessage = remote ? <RemoteHint url={remoteUrl} /> : null;
return ( return (
<Column> <Column>
@ -92,6 +115,7 @@ class Followers extends ImmutablePureComponent {
shouldUpdateScroll={shouldUpdateScroll} shouldUpdateScroll={shouldUpdateScroll}
prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />} prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />}
alwaysPrepend alwaysPrepend
append={remoteMessage}
emptyMessage={emptyMessage} emptyMessage={emptyMessage}
bindToDocument={!multiColumn} bindToDocument={!multiColumn}
> >

View File

@ -17,8 +17,11 @@ import HeaderContainer from '../account_timeline/containers/header_container';
import ColumnBackButton from '../../components/column_back_button'; import ColumnBackButton from '../../components/column_back_button';
import ScrollableList from '../../components/scrollable_list'; import ScrollableList from '../../components/scrollable_list';
import MissingIndicator from 'mastodon/components/missing_indicator'; import MissingIndicator from 'mastodon/components/missing_indicator';
import TimelineHint from 'mastodon/components/timeline_hint';
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
remote: !!(state.getIn(['accounts', props.params.accountId, 'acct']) !== state.getIn(['accounts', props.params.accountId, 'username'])),
remoteUrl: state.getIn(['accounts', props.params.accountId, 'url']),
isAccount: !!state.getIn(['accounts', props.params.accountId]), isAccount: !!state.getIn(['accounts', props.params.accountId]),
accountIds: state.getIn(['user_lists', 'following', props.params.accountId, 'items']), accountIds: state.getIn(['user_lists', 'following', props.params.accountId, 'items']),
hasMore: !!state.getIn(['user_lists', 'following', props.params.accountId, 'next']), hasMore: !!state.getIn(['user_lists', 'following', props.params.accountId, 'next']),
@ -26,6 +29,14 @@ const mapStateToProps = (state, props) => ({
blockedBy: state.getIn(['relationships', props.params.accountId, 'blocked_by'], false), blockedBy: state.getIn(['relationships', props.params.accountId, 'blocked_by'], false),
}); });
const RemoteHint = ({ url }) => (
<TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.follows' defaultMessage='Follows' />} />
);
RemoteHint.propTypes = {
url: PropTypes.string.isRequired,
};
export default @connect(mapStateToProps) export default @connect(mapStateToProps)
class Following extends ImmutablePureComponent { class Following extends ImmutablePureComponent {
@ -38,6 +49,8 @@ class Following extends ImmutablePureComponent {
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
blockedBy: PropTypes.bool, blockedBy: PropTypes.bool,
isAccount: PropTypes.bool, isAccount: PropTypes.bool,
remote: PropTypes.bool,
remoteUrl: PropTypes.string,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
}; };
@ -60,7 +73,7 @@ class Following extends ImmutablePureComponent {
}, 300, { leading: true }); }, 300, { leading: true });
render () { render () {
const { shouldUpdateScroll, accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading } = this.props; const { shouldUpdateScroll, accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, remote, remoteUrl } = this.props;
if (!isAccount) { if (!isAccount) {
return ( return (
@ -78,7 +91,17 @@ class Following extends ImmutablePureComponent {
); );
} }
const emptyMessage = blockedBy ? <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' /> : <FormattedMessage id='account.follows.empty' defaultMessage="This user doesn't follow anyone yet." />; let emptyMessage;
if (blockedBy) {
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
} else if (remote && accountIds.isEmpty()) {
emptyMessage = <RemoteHint url={remoteUrl} />;
} else {
emptyMessage = <FormattedMessage id='account.follows.empty' defaultMessage="This user doesn't follow anyone yet." />;
}
const remoteMessage = remote ? <RemoteHint url={remoteUrl} /> : null;
return ( return (
<Column> <Column>
@ -92,6 +115,7 @@ class Following extends ImmutablePureComponent {
shouldUpdateScroll={shouldUpdateScroll} shouldUpdateScroll={shouldUpdateScroll}
prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />} prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />}
alwaysPrepend alwaysPrepend
append={remoteMessage}
emptyMessage={emptyMessage} emptyMessage={emptyMessage}
bindToDocument={!multiColumn} bindToDocument={!multiColumn}
> >

View File

@ -4,6 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Toggle from 'react-toggle'; import Toggle from 'react-toggle';
import AsyncSelect from 'react-select/async'; import AsyncSelect from 'react-select/async';
import { NonceProvider } from 'react-select';
import SettingToggle from '../../notifications/components/setting_toggle'; import SettingToggle from '../../notifications/components/setting_toggle';
const messages = defineMessages({ const messages = defineMessages({
@ -58,18 +59,20 @@ class ColumnSettings extends React.PureComponent {
{this.modeLabel(mode)} {this.modeLabel(mode)}
</span> </span>
<AsyncSelect <NonceProvider nonce={document.querySelector('meta[name=style-nonce]').content}>
isMulti <AsyncSelect
autoFocus isMulti
value={this.tags(mode)} autoFocus
onChange={this.onSelect(mode)} value={this.tags(mode)}
loadOptions={this.props.onLoad} onChange={this.onSelect(mode)}
className='column-select__container' loadOptions={this.props.onLoad}
classNamePrefix='column-select' className='column-select__container'
name='tags' classNamePrefix='column-select'
placeholder={this.props.intl.formatMessage(messages.placeholder)} name='tags'
noOptionsMessage={this.noOptionsMessage} placeholder={this.props.intl.formatMessage(messages.placeholder)}
/> noOptionsMessage={this.noOptionsMessage}
/>
</NonceProvider>
</div> </div>
); );
} }

View File

@ -88,6 +88,10 @@ class KeyboardShortcuts extends ImmutablePureComponent {
<td><kbd>alt</kbd>+<kbd>n</kbd></td> <td><kbd>alt</kbd>+<kbd>n</kbd></td>
<td><FormattedMessage id='keyboard_shortcuts.toot' defaultMessage='to start a brand new toot' /></td> <td><FormattedMessage id='keyboard_shortcuts.toot' defaultMessage='to start a brand new toot' /></td>
</tr> </tr>
<tr>
<td><kbd>alt</kbd>+<kbd>x</kbd></td>
<td><FormattedMessage id='keyboard_shortcuts.spoilers' defaultMessage='to show/hide CW field' /></td>
</tr>
<tr> <tr>
<td><kbd>backspace</kbd></td> <td><kbd>backspace</kbd></td>
<td><FormattedMessage id='keyboard_shortcuts.back' defaultMessage='to navigate back' /></td> <td><FormattedMessage id='keyboard_shortcuts.back' defaultMessage='to navigate back' /></td>

View File

@ -201,10 +201,6 @@ class ActionBar extends React.PureComponent {
if (me === status.getIn(['account', 'id'])) { if (me === status.getIn(['account', 'id'])) {
if (publicStatus) { if (publicStatus) {
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
} else {
if (status.get('visibility') === 'private') {
menu.push({ text: intl.formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog_private), action: this.handleReblogClick });
}
} }
menu.push(null); menu.push(null);
@ -261,14 +257,23 @@ class ActionBar extends React.PureComponent {
replyIcon = 'reply-all'; replyIcon = 'reply-all';
} }
let reblogIcon = 'retweet'; const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
if (status.get('visibility') === 'direct') reblogIcon = 'envelope';
else if (status.get('visibility') === 'private') reblogIcon = 'lock'; let reblogTitle;
if (status.get('reblogged')) {
reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
} else if (publicStatus) {
reblogTitle = intl.formatMessage(messages.reblog);
} else if (reblogPrivate) {
reblogTitle = intl.formatMessage(messages.reblog_private);
} else {
reblogTitle = intl.formatMessage(messages.cannot_reblog);
}
return ( return (
<div className='detailed-status__action-bar'> <div className='detailed-status__action-bar'>
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /></div> <div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /></div>
<div className='detailed-status__button'><IconButton disabled={!publicStatus} active={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div> <div className='detailed-status__button'><IconButton disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} /></div>
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div> <div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
{shareButton} {shareButton}
<div className='detailed-status__button'><IconButton className='bookmark-icon' active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div> <div className='detailed-status__button'><IconButton className='bookmark-icon' active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div>

View File

@ -2,9 +2,13 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Immutable from 'immutable'; import Immutable from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage } from 'react-intl';
import punycode from 'punycode'; import punycode from 'punycode';
import classnames from 'classnames'; import classnames from 'classnames';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import { useBlurhash } from 'mastodon/initial_state';
import { decode } from 'blurhash';
import { debounce } from 'lodash';
const IDNA_PREFIX = 'xn--'; const IDNA_PREFIX = 'xn--';
@ -63,6 +67,7 @@ export default class Card extends React.PureComponent {
compact: PropTypes.bool, compact: PropTypes.bool,
defaultWidth: PropTypes.number, defaultWidth: PropTypes.number,
cacheWidth: PropTypes.func, cacheWidth: PropTypes.func,
sensitive: PropTypes.bool,
}; };
static defaultProps = { static defaultProps = {
@ -72,15 +77,72 @@ export default class Card extends React.PureComponent {
state = { state = {
width: this.props.defaultWidth || 280, width: this.props.defaultWidth || 280,
previewLoaded: false,
embedded: false, embedded: false,
revealed: !this.props.sensitive,
}; };
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
if (!Immutable.is(this.props.card, nextProps.card)) { if (!Immutable.is(this.props.card, nextProps.card)) {
this.setState({ embedded: false }); this.setState({ embedded: false, previewLoaded: false });
}
if (this.props.sensitive !== nextProps.sensitive) {
this.setState({ revealed: !nextProps.sensitive });
} }
} }
componentDidMount () {
window.addEventListener('resize', this.handleResize, { passive: true });
if (this.props.card && this.props.card.get('blurhash') && this.canvas) {
this._decode();
}
}
componentWillUnmount () {
window.removeEventListener('resize', this.handleResize);
}
componentDidUpdate (prevProps) {
const { card } = this.props;
if (card.get('blurhash') && (!prevProps.card || prevProps.card.get('blurhash') !== card.get('blurhash')) && this.canvas) {
this._decode();
}
}
_decode () {
if (!useBlurhash) return;
const hash = this.props.card.get('blurhash');
const pixels = decode(hash, 32, 32);
if (pixels) {
const ctx = this.canvas.getContext('2d');
const imageData = new ImageData(pixels, 32, 32);
ctx.putImageData(imageData, 0, 0);
}
}
_setDimensions () {
const width = this.node.offsetWidth;
if (this.props.cacheWidth) {
this.props.cacheWidth(width);
}
this.setState({ width });
}
handleResize = debounce(() => {
if (this.node) {
this._setDimensions();
}
}, 250, {
trailing: true,
});
handlePhotoClick = () => { handlePhotoClick = () => {
const { card, onOpenMedia } = this.props; const { card, onOpenMedia } = this.props;
@ -113,12 +175,27 @@ export default class Card extends React.PureComponent {
} }
setRef = c => { setRef = c => {
if (c) { this.node = c;
if (this.props.cacheWidth) this.props.cacheWidth(c.offsetWidth);
this.setState({ width: c.offsetWidth }); if (this.node) {
this._setDimensions();
} }
} }
setCanvasRef = c => {
this.canvas = c;
}
handleImageLoad = () => {
this.setState({ previewLoaded: true });
}
handleReveal = e => {
e.preventDefault();
e.stopPropagation();
this.setState({ revealed: true });
}
renderVideo () { renderVideo () {
const { card } = this.props; const { card } = this.props;
const content = { __html: addAutoPlay(card.get('html')) }; const content = { __html: addAutoPlay(card.get('html')) };
@ -138,7 +215,7 @@ export default class Card extends React.PureComponent {
render () { render () {
const { card, maxDescription, compact } = this.props; const { card, maxDescription, compact } = this.props;
const { width, embedded } = this.state; const { width, embedded, revealed } = this.state;
if (card === null) { if (card === null) {
return null; return null;
@ -161,7 +238,18 @@ export default class Card extends React.PureComponent {
); );
let embed = ''; let embed = '';
let thumbnail = <div style={{ backgroundImage: `url(${card.get('image')})`, width: horizontal ? width : null, height: horizontal ? height : null }} className='status-card__image-image' />; let canvas = <canvas width={32} height={32} ref={this.setCanvasRef} className={classnames('status-card__image-preview', { 'status-card__image-preview--hidden' : revealed && this.state.previewLoaded })} />;
let thumbnail = <img src={card.get('image')} alt='' style={{ width: horizontal ? width : null, height: horizontal ? height : null, visibility: revealed ? null : 'hidden' }} onLoad={this.handleImageLoad} className='status-card__image-image' />;
let spoilerButton = (
<button type='button' onClick={this.handleReveal} className='spoiler-button__overlay'>
<span className='spoiler-button__overlay__label'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
</button>
);
spoilerButton = (
<div className={classnames('spoiler-button', { 'spoiler-button--minified': revealed })}>
{spoilerButton}
</div>
);
if (interactive) { if (interactive) {
if (embedded) { if (embedded) {
@ -175,20 +263,24 @@ export default class Card extends React.PureComponent {
embed = ( embed = (
<div className='status-card__image'> <div className='status-card__image'>
{canvas}
{thumbnail} {thumbnail}
<div className='status-card__actions'> {revealed && (
<div> <div className='status-card__actions'>
<button onClick={this.handleEmbedClick}><Icon id={iconVariant} /></button> <div>
{horizontal && <a href={card.get('url')} target='_blank' rel='noopener noreferrer'><Icon id='external-link' /></a>} <button onClick={this.handleEmbedClick}><Icon id={iconVariant} /></button>
{horizontal && <a href={card.get('url')} target='_blank' rel='noopener noreferrer'><Icon id='external-link' /></a>}
</div>
</div> </div>
</div> )}
{!revealed && spoilerButton}
</div> </div>
); );
} }
return ( return (
<div className={className} ref={this.setRef}> <div className={className} ref={this.setRef} onClick={revealed ? null : this.handleReveal} role={revealed ? 'button' : null}>
{embed} {embed}
{!compact && description} {!compact && description}
</div> </div>
@ -196,6 +288,7 @@ export default class Card extends React.PureComponent {
} else if (card.get('image')) { } else if (card.get('image')) {
embed = ( embed = (
<div className='status-card__image'> <div className='status-card__image'>
{canvas}
{thumbnail} {thumbnail}
</div> </div>
); );

View File

@ -6,7 +6,7 @@ import DisplayName from '../../../components/display_name';
import StatusContent from '../../../components/status_content'; import StatusContent from '../../../components/status_content';
import MediaGallery from '../../../components/media_gallery'; import MediaGallery from '../../../components/media_gallery';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { FormattedDate } from 'react-intl'; import { injectIntl, defineMessages, FormattedDate } from 'react-intl';
import Card from './card'; import Card from './card';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import Video from '../../video'; import Video from '../../video';
@ -16,7 +16,15 @@ import classNames from 'classnames';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import AnimatedNumber from 'mastodon/components/animated_number'; import AnimatedNumber from 'mastodon/components/animated_number';
export default class DetailedStatus extends ImmutablePureComponent { const messages = defineMessages({
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
});
export default @injectIntl
class DetailedStatus extends ImmutablePureComponent {
static contextTypes = { static contextTypes = {
router: PropTypes.object, router: PropTypes.object,
@ -92,7 +100,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
render () { render () {
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status; const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
const outerStyle = { boxSizing: 'border-box' }; const outerStyle = { boxSizing: 'border-box' };
const { compact } = this.props; const { intl, compact } = this.props;
if (!status) { if (!status) {
return null; return null;
@ -117,8 +125,11 @@ export default class DetailedStatus extends ImmutablePureComponent {
src={attachment.get('url')} src={attachment.get('url')}
alt={attachment.get('description')} alt={attachment.get('description')}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)} duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
height={110} poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
preload backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
height={150}
/> />
); );
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
@ -153,38 +164,48 @@ export default class DetailedStatus extends ImmutablePureComponent {
); );
} }
} else if (status.get('spoiler_text').length === 0) { } else if (status.get('spoiler_text').length === 0) {
media = <Card onOpenMedia={this.props.onOpenMedia} card={status.get('card', null)} />; media = <Card sensitive={status.get('sensitive')} onOpenMedia={this.props.onOpenMedia} card={status.get('card', null)} />;
} }
if (status.get('application')) { if (status.get('application')) {
applicationLink = <span> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></span>; applicationLink = <React.Fragment> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></React.Fragment>;
} }
if (status.get('visibility') === 'direct') { const visibilityIconInfo = {
reblogIcon = 'envelope'; 'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) },
} else if (status.get('visibility') === 'private') { 'unlisted': { icon: 'unlock', text: intl.formatMessage(messages.unlisted_short) },
reblogIcon = 'lock'; 'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) },
} 'direct': { icon: 'envelope', text: intl.formatMessage(messages.direct_short) },
};
const visibilityIcon = visibilityIconInfo[status.get('visibility')];
const visibilityLink = <React.Fragment> · <Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></React.Fragment>;
if (['private', 'direct'].includes(status.get('visibility'))) { if (['private', 'direct'].includes(status.get('visibility'))) {
reblogLink = <Icon id={reblogIcon} />; reblogLink = '';
} else if (this.context.router) { } else if (this.context.router) {
reblogLink = ( reblogLink = (
<Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'> <React.Fragment>
<Icon id={reblogIcon} /> <React.Fragment> · </React.Fragment>
<span className='detailed-status__reblogs'> <Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'>
<AnimatedNumber value={status.get('reblogs_count')} /> <Icon id={reblogIcon} />
</span> <span className='detailed-status__reblogs'>
</Link> <AnimatedNumber value={status.get('reblogs_count')} />
</span>
</Link>
</React.Fragment>
); );
} else { } else {
reblogLink = ( reblogLink = (
<a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}> <React.Fragment>
<Icon id={reblogIcon} /> <React.Fragment> · </React.Fragment>
<span className='detailed-status__reblogs'> <a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
<AnimatedNumber value={status.get('reblogs_count')} /> <Icon id={reblogIcon} />
</span> <span className='detailed-status__reblogs'>
</a> <AnimatedNumber value={status.get('reblogs_count')} />
</span>
</a>
</React.Fragment>
); );
} }
@ -210,7 +231,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
return ( return (
<div style={outerStyle}> <div style={outerStyle}>
<div ref={this.setRef} className={classNames('detailed-status', { compact })}> <div ref={this.setRef} className={classNames('detailed-status', `detailed-status-${status.get('visibility')}`, { compact })}>
<a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name'> <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name'>
<div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={48} /></div> <div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={48} /></div>
<DisplayName account={status.get('account')} localDomain={this.props.domain} /> <DisplayName account={status.get('account')} localDomain={this.props.domain} />
@ -223,7 +244,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
<div className='detailed-status__meta'> <div className='detailed-status__meta'>
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'> <a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'>
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' /> <FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
</a>{applicationLink} · {reblogLink} · {favouriteLink} </a>{visibilityLink}{applicationLink}{reblogLink} · {favouriteLink}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,25 +1,24 @@
import { render, fireEvent, screen } from '@testing-library/react';
import React from 'react'; import React from 'react';
import { mount } from 'enzyme';
import Column from '../column'; import Column from '../column';
import ColumnHeader from '../column_header';
describe('<Column />', () => { describe('<Column />', () => {
describe('<ColumnHeader /> click handler', () => { describe('<ColumnHeader /> click handler', () => {
it('runs the scroll animation if the column contains scrollable content', () => { it('runs the scroll animation if the column contains scrollable content', () => {
const wrapper = mount( const scrollToMock = jest.fn();
const { container } = render(
<Column heading='notifications'> <Column heading='notifications'>
<div className='scrollable' /> <div className='scrollable' />
</Column>, </Column>,
); );
const scrollToMock = jest.fn(); container.querySelector('.scrollable').scrollTo = scrollToMock;
wrapper.find(Column).find('.scrollable').getDOMNode().scrollTo = scrollToMock; fireEvent.click(screen.getByText('notifications'));
wrapper.find(ColumnHeader).find('button').simulate('click');
expect(scrollToMock).toHaveBeenCalledWith({ behavior: 'smooth', top: 0 }); expect(scrollToMock).toHaveBeenCalledWith({ behavior: 'smooth', top: 0 });
}); });
it('does not try to scroll if there is no scrollable content', () => { it('does not try to scroll if there is no scrollable content', () => {
const wrapper = mount(<Column heading='notifications' />); render(<Column heading='notifications' />);
wrapper.find(ColumnHeader).find('button').simulate('click'); fireEvent.click(screen.getByText('notifications'));
}); });
}); });
}); });

View File

@ -59,8 +59,11 @@ export default class AudioModal extends ImmutablePureComponent {
src={media.get('url')} src={media.get('url')}
alt={media.get('description')} alt={media.get('description')}
duration={media.getIn(['meta', 'original', 'duration'], 0)} duration={media.getIn(['meta', 'original', 'duration'], 0)}
height={135} height={150}
preload poster={media.get('preview_url') || status.getIn(['account', 'avatar_static'])}
backgroundColor={media.getIn(['meta', 'colors', 'background'])}
foregroundColor={media.getIn(['meta', 'colors', 'foreground'])}
accentColor={media.getIn(['meta', 'colors', 'accent'])}
/> />
</div> </div>

View File

@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import classNames from 'classnames'; import classNames from 'classnames';
import { changeUploadCompose } from '../../../actions/compose'; import { changeUploadCompose, uploadThumbnail } from '../../../actions/compose';
import { getPointerPosition } from '../../video'; import { getPointerPosition } from '../../video';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import IconButton from 'mastodon/components/icon_button'; import IconButton from 'mastodon/components/icon_button';
@ -17,15 +17,19 @@ import CharacterCounter from 'mastodon/features/compose/components/character_cou
import { length } from 'stringz'; import { length } from 'stringz';
import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components'; import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components';
import GIFV from 'mastodon/components/gifv'; import GIFV from 'mastodon/components/gifv';
import { me } from 'mastodon/initial_state';
const messages = defineMessages({ const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' }, close: { id: 'lightbox.close', defaultMessage: 'Close' },
apply: { id: 'upload_modal.apply', defaultMessage: 'Apply' }, apply: { id: 'upload_modal.apply', defaultMessage: 'Apply' },
placeholder: { id: 'upload_modal.description_placeholder', defaultMessage: 'A quick brown fox jumps over the lazy dog' }, placeholder: { id: 'upload_modal.description_placeholder', defaultMessage: 'A quick brown fox jumps over the lazy dog' },
chooseImage: { id: 'upload_modal.choose_image', defaultMessage: 'Choose image' },
}); });
const mapStateToProps = (state, { id }) => ({ const mapStateToProps = (state, { id }) => ({
media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id), media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
account: state.getIn(['accounts', me]),
isUploadingThumbnail: state.getIn(['compose', 'isUploadingThumbnail']),
}); });
const mapDispatchToProps = (dispatch, { id }) => ({ const mapDispatchToProps = (dispatch, { id }) => ({
@ -34,6 +38,10 @@ const mapDispatchToProps = (dispatch, { id }) => ({
dispatch(changeUploadCompose(id, { description, focus: `${x.toFixed(2)},${y.toFixed(2)}` })); dispatch(changeUploadCompose(id, { description, focus: `${x.toFixed(2)},${y.toFixed(2)}` }));
}, },
onSelectThumbnail: files => {
dispatch(uploadThumbnail(id, files[0]));
},
}); });
const removeExtraLineBreaks = str => str.replace(/\n\n/g, '******') const removeExtraLineBreaks = str => str.replace(/\n\n/g, '******')
@ -78,6 +86,10 @@ class FocalPointModal extends ImmutablePureComponent {
static propTypes = { static propTypes = {
media: ImmutablePropTypes.map.isRequired, media: ImmutablePropTypes.map.isRequired,
account: ImmutablePropTypes.map.isRequired,
isUploadingThumbnail: PropTypes.bool,
onSave: PropTypes.func.isRequired,
onSelectThumbnail: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
@ -232,13 +244,29 @@ class FocalPointModal extends ImmutablePureComponent {
}).catch(() => this.setState({ detecting: false })); }).catch(() => this.setState({ detecting: false }));
} }
handleThumbnailChange = e => {
if (e.target.files.length > 0) {
this.setState({ dirty: true });
this.props.onSelectThumbnail(e.target.files);
}
}
setFileInputRef = c => {
this.fileInput = c;
}
handleFileInputClick = () => {
this.fileInput.click();
}
render () { render () {
const { media, intl, onClose } = this.props; const { media, intl, account, onClose, isUploadingThumbnail } = this.props;
const { x, y, dragging, description, dirty, detecting, progress } = this.state; const { x, y, dragging, description, dirty, detecting, progress } = this.state;
const width = media.getIn(['meta', 'original', 'width']) || null; const width = media.getIn(['meta', 'original', 'width']) || null;
const height = media.getIn(['meta', 'original', 'height']) || null; const height = media.getIn(['meta', 'original', 'height']) || null;
const focals = ['image', 'gifv'].includes(media.get('type')); const focals = ['image', 'gifv'].includes(media.get('type'));
const thumbnailable = ['audio', 'video'].includes(media.get('type'));
const previewRatio = 16/9; const previewRatio = 16/9;
const previewWidth = 200; const previewWidth = 200;
@ -265,6 +293,30 @@ class FocalPointModal extends ImmutablePureComponent {
<div className='report-modal__comment'> <div className='report-modal__comment'>
{focals && <p><FormattedMessage id='upload_modal.hint' defaultMessage='Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.' /></p>} {focals && <p><FormattedMessage id='upload_modal.hint' defaultMessage='Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.' /></p>}
{thumbnailable && (
<React.Fragment>
<label className='setting-text-label' htmlFor='upload-modal__thumbnail'><FormattedMessage id='upload_form.thumbnail' defaultMessage='Change thumbnail' /></label>
<Button disabled={isUploadingThumbnail} text={intl.formatMessage(messages.chooseImage)} onClick={this.handleFileInputClick} />
<label>
<span style={{ display: 'none' }}>{intl.formatMessage(messages.chooseImage)}</span>
<input
id='upload-modal__thumbnail'
ref={this.setFileInputRef}
type='file'
accept='image/png,image/jpeg'
onChange={this.handleThumbnailChange}
style={{ display: 'none' }}
disabled={isUploadingThumbnail}
/>
</label>
<hr className='setting-divider' />
</React.Fragment>
)}
<label className='setting-text-label' htmlFor='upload-modal__description'> <label className='setting-text-label' htmlFor='upload-modal__description'>
{descriptionLabel} {descriptionLabel}
</label> </label>
@ -290,7 +342,7 @@ class FocalPointModal extends ImmutablePureComponent {
<CharacterCounter max={1500} text={detecting ? '' : description} /> <CharacterCounter max={1500} text={detecting ? '' : description} />
</div> </div>
<Button disabled={!dirty || detecting || length(description) > 1500} text={intl.formatMessage(messages.apply)} onClick={this.handleSubmit} /> <Button disabled={!dirty || detecting || isUploadingThumbnail || length(description) > 1500} text={intl.formatMessage(messages.apply)} onClick={this.handleSubmit} />
</div> </div>
<div className='focal-point-modal__content'> <div className='focal-point-modal__content'>
@ -325,7 +377,10 @@ class FocalPointModal extends ImmutablePureComponent {
src={media.get('url')} src={media.get('url')}
duration={media.getIn(['meta', 'original', 'duration'], 0)} duration={media.getIn(['meta', 'original', 'duration'], 0)}
height={150} height={150}
preload poster={media.get('preview_url') || account.get('avatar_static')}
backgroundColor={media.getIn(['meta', 'colors', 'background'])}
foregroundColor={media.getIn(['meta', 'colors', 'foreground'])}
accentColor={media.getIn(['meta', 'colors', 'accent'])}
editable editable
/> />
)} )}

View File

@ -17,6 +17,8 @@ const makeGetStatusIds = (pending = false) => createSelector([
const statusForId = statuses.get(id); const statusForId = statuses.get(id);
let showStatus = true; let showStatus = true;
if (statusForId.get('account') === me) return true;
if (columnSettings.getIn(['shows', 'reblog']) === false) { if (columnSettings.getIn(['shows', 'reblog']) === false) {
showStatus = showStatus && statusForId.get('reblog') === null; showStatus = showStatus && statusForId.get('reblog') === null;
} }

View File

@ -10,7 +10,7 @@ import LoadingBarContainer from './containers/loading_bar_container';
import ModalContainer from './containers/modal_container'; import ModalContainer from './containers/modal_container';
import { isMobile } from '../../is_mobile'; import { isMobile } from '../../is_mobile';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { uploadCompose, resetCompose } from '../../actions/compose'; import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
import { expandHomeTimeline } from '../../actions/timelines'; import { expandHomeTimeline } from '../../actions/timelines';
import { expandNotifications } from '../../actions/notifications'; import { expandNotifications } from '../../actions/notifications';
import { fetchFilters } from '../../actions/filters'; import { fetchFilters } from '../../actions/filters';
@ -76,6 +76,7 @@ const keyMap = {
new: 'n', new: 'n',
search: 's', search: 's',
forceNew: 'option+n', forceNew: 'option+n',
toggleComposeSpoilers: 'option+x',
focusColumn: ['1', '2', '3', '4', '5', '6', '7', '8', '9'], focusColumn: ['1', '2', '3', '4', '5', '6', '7', '8', '9'],
reply: 'r', reply: 'r',
favourite: 'f', favourite: 'f',
@ -375,7 +376,7 @@ class UI extends React.PureComponent {
componentDidMount () { componentDidMount () {
this.hotkeys.__mousetrap__.stopCallback = (e, element) => { this.hotkeys.__mousetrap__.stopCallback = (e, element) => {
return ['TEXTAREA', 'SELECT', 'INPUT'].includes(element.tagName); return ['TEXTAREA', 'SELECT', 'INPUT'].includes(element.tagName) && !e.altKey;
}; };
} }
@ -420,6 +421,11 @@ class UI extends React.PureComponent {
this.props.dispatch(resetCompose()); this.props.dispatch(resetCompose());
} }
handleHotkeyToggleComposeSpoilers = e => {
e.preventDefault();
this.props.dispatch(changeComposeSpoilerness());
}
handleHotkeyFocusColumn = e => { handleHotkeyFocusColumn = e => {
const index = (e.key * 1) + 1; // First child is drawer, skip that const index = (e.key * 1) + 1; // First child is drawer, skip that
const column = this.node.querySelector(`.column:nth-child(${index})`); const column = this.node.querySelector(`.column:nth-child(${index})`);
@ -515,6 +521,7 @@ class UI extends React.PureComponent {
new: this.handleHotkeyNew, new: this.handleHotkeyNew,
search: this.handleHotkeySearch, search: this.handleHotkeySearch,
forceNew: this.handleHotkeyForceNew, forceNew: this.handleHotkeyForceNew,
toggleComposeSpoilers: this.handleHotkeyToggleComposeSpoilers,
focusColumn: this.handleHotkeyFocusColumn, focusColumn: this.handleHotkeyFocusColumn,
back: this.handleHotkeyBack, back: this.handleHotkeyBack,
goToHome: this.handleHotkeyGoToHome, goToHome: this.handleHotkeyGoToHome,

View File

@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { fromJS, is } from 'immutable'; import { fromJS, is } from 'immutable';
import { throttle } from 'lodash'; import { throttle, debounce } from 'lodash';
import classNames from 'classnames'; import classNames from 'classnames';
import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen'; import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
import { displayMedia, useBlurhash } from '../../initial_state'; import { displayMedia, useBlurhash } from '../../initial_state';
@ -19,7 +19,6 @@ const messages = defineMessages({
close: { id: 'video.close', defaultMessage: 'Close video' }, close: { id: 'video.close', defaultMessage: 'Close video' },
fullscreen: { id: 'video.fullscreen', defaultMessage: 'Full screen' }, fullscreen: { id: 'video.fullscreen', defaultMessage: 'Full screen' },
exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' }, exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' },
download: { id: 'video.download', defaultMessage: 'Download file' },
}); });
export const formatTime = secondsNum => { export const formatTime = secondsNum => {
@ -87,6 +86,14 @@ export const getPointerPosition = (el, event) => {
return position; return position;
}; };
export const fileNameFromURL = str => {
const url = new URL(str);
const pathname = url.pathname;
const index = pathname.lastIndexOf('/');
return pathname.substring(index + 1);
};
export default @injectIntl export default @injectIntl
class Video extends React.PureComponent { class Video extends React.PureComponent {
@ -126,29 +133,26 @@ class Video extends React.PureComponent {
revealed: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'), revealed: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
}; };
// Hard-coded in components.scss
// Any way to get ::before values programatically?
volWidth = 50;
volOffset = 70;
volHandleOffset = v => {
const offset = v * this.volWidth + this.volOffset;
return (offset > 110) ? 110 : offset;
}
setPlayerRef = c => { setPlayerRef = c => {
this.player = c; this.player = c;
if (c) { if (this.player) {
if (this.props.cacheWidth) this.props.cacheWidth(this.player.offsetWidth); this._setDimensions();
this.setState({
containerWidth: c.offsetWidth,
});
} }
} }
_setDimensions () {
const width = this.player.offsetWidth;
if (this.props.cacheWidth) {
this.props.cacheWidth(width);
}
this.setState({
containerWidth: width,
});
}
setVideoRef = c => { setVideoRef = c => {
this.video = c; this.video = c;
@ -173,15 +177,26 @@ class Video extends React.PureComponent {
handlePlay = () => { handlePlay = () => {
this.setState({ paused: false }); this.setState({ paused: false });
this._updateTime();
} }
handlePause = () => { handlePause = () => {
this.setState({ paused: true }); this.setState({ paused: true });
} }
_updateTime () {
requestAnimationFrame(() => {
this.handleTimeUpdate();
if (!this.state.paused) {
this._updateTime();
}
});
}
handleTimeUpdate = () => { handleTimeUpdate = () => {
this.setState({ this.setState({
currentTime: Math.floor(this.video.currentTime), currentTime: this.video.currentTime,
duration: Math.floor(this.video.duration), duration: Math.floor(this.video.duration),
}); });
} }
@ -206,22 +221,14 @@ class Video extends React.PureComponent {
} }
handleMouseVolSlide = throttle(e => { handleMouseVolSlide = throttle(e => {
const rect = this.volume.getBoundingClientRect(); const { x } = getPointerPosition(this.volume, e);
const x = (e.clientX - rect.left) / this.volWidth; //x position within the element.
if(!isNaN(x)) { if(!isNaN(x)) {
let slideamt = x; this.setState({ volume: x }, () => {
this.video.volume = x;
if(x > 1) { });
slideamt = 1;
} else if(x < 0) {
slideamt = 0;
}
this.video.volume = slideamt;
this.setState({ volume: slideamt });
} }
}, 60); }, 15);
handleMouseDown = e => { handleMouseDown = e => {
document.addEventListener('mousemove', this.handleMouseMove, true); document.addEventListener('mousemove', this.handleMouseMove, true);
@ -249,13 +256,14 @@ class Video extends React.PureComponent {
handleMouseMove = throttle(e => { handleMouseMove = throttle(e => {
const { x } = getPointerPosition(this.seek, e); const { x } = getPointerPosition(this.seek, e);
const currentTime = Math.floor(this.video.duration * x); const currentTime = this.video.duration * x;
if (!isNaN(currentTime)) { if (!isNaN(currentTime)) {
this.video.currentTime = currentTime; this.setState({ currentTime }, () => {
this.setState({ currentTime }); this.video.currentTime = currentTime;
});
} }
}, 60); }, 15);
togglePlay = () => { togglePlay = () => {
if (this.state.paused) { if (this.state.paused) {
@ -280,6 +288,7 @@ class Video extends React.PureComponent {
document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true); document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
window.addEventListener('scroll', this.handleScroll); window.addEventListener('scroll', this.handleScroll);
window.addEventListener('resize', this.handleResize, { passive: true });
if (this.props.blurhash) { if (this.props.blurhash) {
this._decode(); this._decode();
@ -288,6 +297,7 @@ class Video extends React.PureComponent {
componentWillUnmount () { componentWillUnmount () {
window.removeEventListener('scroll', this.handleScroll); window.removeEventListener('scroll', this.handleScroll);
window.removeEventListener('resize', this.handleResize);
document.removeEventListener('fullscreenchange', this.handleFullscreenChange, true); document.removeEventListener('fullscreenchange', this.handleFullscreenChange, true);
document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange, true); document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);
@ -325,6 +335,14 @@ class Video extends React.PureComponent {
} }
} }
handleResize = debounce(() => {
if (this.player) {
this._setDimensions();
}
}, 250, {
trailing: true,
});
handleScroll = throttle(() => { handleScroll = throttle(() => {
if (!this.video) { if (!this.video) {
return; return;
@ -381,8 +399,10 @@ class Video extends React.PureComponent {
} }
handleProgress = () => { handleProgress = () => {
if (this.video.buffered.length > 0) { const lastTimeRange = this.video.buffered.length - 1;
this.setState({ buffer: this.video.buffered.end(0) / this.video.duration * 100 });
if (lastTimeRange > -1) {
this.setState({ buffer: Math.ceil(this.video.buffered.end(lastTimeRange) / this.video.duration * 100) });
} }
} }
@ -421,9 +441,6 @@ class Video extends React.PureComponent {
const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, link, editable } = this.props; const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, link, editable } = this.props;
const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state; const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
const progress = (currentTime / duration) * 100; const progress = (currentTime / duration) * 100;
const volumeWidth = (muted) ? 0 : volume * this.volWidth;
const volumeHandleLoc = (muted) ? this.volHandleOffset(0) : this.volHandleOffset(volume);
const playerStyle = {}; const playerStyle = {};
let { width, height } = this.props; let { width, height } = this.props;
@ -481,7 +498,6 @@ class Video extends React.PureComponent {
onClick={this.togglePlay} onClick={this.togglePlay}
onPlay={this.handlePlay} onPlay={this.handlePlay}
onPause={this.handlePause} onPause={this.handlePause}
onTimeUpdate={this.handleTimeUpdate}
onLoadedData={this.handleLoadedData} onLoadedData={this.handleLoadedData}
onProgress={this.handleProgress} onProgress={this.handleProgress}
onVolumeChange={this.handleVolumeChange} onVolumeChange={this.handleVolumeChange}
@ -510,19 +526,19 @@ class Video extends React.PureComponent {
<button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay} autoFocus={detailed}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button> <button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay} autoFocus={detailed}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
<button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button> <button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
<div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}> <div className={classNames('video-player__volume', { active: this.state.hovered })} onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
&nbsp; <div className='video-player__volume__current' style={{ width: `${volume * 100}%` }} />
<div className='video-player__volume__current' style={{ width: `${volumeWidth}px` }} />
<span <span
className={classNames('video-player__volume__handle')} className={classNames('video-player__volume__handle')}
tabIndex='0' tabIndex='0'
style={{ left: `${volumeHandleLoc}px` }} style={{ left: `${volume * 100}%` }}
/> />
</div> </div>
{(detailed || fullscreen) && ( {(detailed || fullscreen) && (
<span> <span className='video-player__time'>
<span className='video-player__time-current'>{formatTime(currentTime)}</span> <span className='video-player__time-current'>{formatTime(Math.floor(currentTime))}</span>
<span className='video-player__time-sep'>/</span> <span className='video-player__time-sep'>/</span>
<span className='video-player__time-total'>{formatTime(duration)}</span> <span className='video-player__time-total'>{formatTime(duration)}</span>
</span> </span>
@ -535,7 +551,6 @@ class Video extends React.PureComponent {
{(!onCloseVideo && !editable && !fullscreen) && <button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>} {(!onCloseVideo && !editable && !fullscreen) && <button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>}
{(!fullscreen && onOpenVideo) && <button type='button' title={intl.formatMessage(messages.expand)} aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><Icon id='expand' fixedWidth /></button>} {(!fullscreen && onOpenVideo) && <button type='button' title={intl.formatMessage(messages.expand)} aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><Icon id='expand' fixedWidth /></button>}
{onCloseVideo && <button type='button' title={intl.formatMessage(messages.close)} aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>} {onCloseVideo && <button type='button' title={intl.formatMessage(messages.close)} aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>}
<button type='button' title={intl.formatMessage(messages.download)} aria-label={intl.formatMessage(messages.download)}><a className='video-player__download__icon' href={this.props.src} download><Icon id={'download'} fixedWidth /></a></button>
<button type='button' title={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><Icon id={fullscreen ? 'compress' : 'arrows-alt'} fixedWidth /></button> <button type='button' title={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><Icon id={fullscreen ? 'compress' : 'arrows-alt'} fixedWidth /></button>
</div> </div>
</div> </div>

View File

@ -1,10 +1,13 @@
{ {
"account.account_note_header": "Your note for @{name}",
"account.add_account_note": "Add note for @{name}",
"account.add_or_remove_from_list": "أضفه أو أزله من القائمة", "account.add_or_remove_from_list": "أضفه أو أزله من القائمة",
"account.badges.bot": "روبوت", "account.badges.bot": "روبوت",
"account.badges.group": "فريق", "account.badges.group": "فريق",
"account.block": "حظر @{name}", "account.block": "حظر @{name}",
"account.block_domain": "إخفاء كل شيء قادم من اسم النطاق {domain}", "account.block_domain": "إخفاء كل شيء قادم من اسم النطاق {domain}",
"account.blocked": "محظور", "account.blocked": "محظور",
"account.browse_more_on_origin_server": "Browse more on the original profile",
"account.cancel_follow_request": "إلغاء طلب المتابَعة", "account.cancel_follow_request": "إلغاء طلب المتابَعة",
"account.direct": "رسالة خاصة إلى @{name}", "account.direct": "رسالة خاصة إلى @{name}",
"account.domain_blocked": "النطاق مخفي", "account.domain_blocked": "النطاق مخفي",
@ -39,6 +42,10 @@
"account.unfollow": "إلغاء المتابعة", "account.unfollow": "إلغاء المتابعة",
"account.unmute": "إلغاء الكتم عن @{name}", "account.unmute": "إلغاء الكتم عن @{name}",
"account.unmute_notifications": "إلغاء كتم إخطارات @{name}", "account.unmute_notifications": "إلغاء كتم إخطارات @{name}",
"account_note.cancel": "Cancel",
"account_note.edit": "Edit",
"account_note.placeholder": "No comment provided",
"account_note.save": "Save",
"alert.rate_limited.message": "يرجى إعادة المحاولة بعد {retry_time, time, medium}.", "alert.rate_limited.message": "يرجى إعادة المحاولة بعد {retry_time, time, medium}.",
"alert.rate_limited.title": "المعدل محدود", "alert.rate_limited.title": "المعدل محدود",
"alert.unexpected.message": "لقد طرأ هناك خطأ غير متوقّع.", "alert.unexpected.message": "لقد طرأ هناك خطأ غير متوقّع.",
@ -74,9 +81,9 @@
"column_header.show_settings": "عرض الإعدادات", "column_header.show_settings": "عرض الإعدادات",
"column_header.unpin": "فك التدبيس", "column_header.unpin": "فك التدبيس",
"column_subheading.settings": "الإعدادات", "column_subheading.settings": "الإعدادات",
"community.column_settings.local_only": "Local only", "community.column_settings.local_only": "المحلي فقط",
"community.column_settings.media_only": "الوسائط فقط", "community.column_settings.media_only": "الوسائط فقط",
"community.column_settings.remote_only": "Remote only", "community.column_settings.remote_only": "عن بُعد فقط",
"compose_form.direct_message_warning": "لن يَظهر هذا التبويق إلا للمستخدمين المذكورين.", "compose_form.direct_message_warning": "لن يَظهر هذا التبويق إلا للمستخدمين المذكورين.",
"compose_form.direct_message_warning_learn_more": "اقرأ المزيد", "compose_form.direct_message_warning_learn_more": "اقرأ المزيد",
"compose_form.hashtag_warning": "هذا التبويق لن يُدرَج تحت أي وسم كان بما أنه غير مُدرَج. لا يُسمح بالبحث إلّا عن التبويقات العمومية عن طريق الوسوم.", "compose_form.hashtag_warning": "هذا التبويق لن يُدرَج تحت أي وسم كان بما أنه غير مُدرَج. لا يُسمح بالبحث إلّا عن التبويقات العمومية عن طريق الوسوم.",
@ -110,7 +117,7 @@
"confirmations.logout.confirm": "خروج", "confirmations.logout.confirm": "خروج",
"confirmations.logout.message": "متأكد من أنك تريد الخروج؟", "confirmations.logout.message": "متأكد من أنك تريد الخروج؟",
"confirmations.mute.confirm": "أكتم", "confirmations.mute.confirm": "أكتم",
"confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", "confirmations.mute.explanation": "هذا سيخفي المنشورات عنهم وتلك المشار فيها إليهم، لكنه سيسمح لهم برؤية منشوراتك ومتابعتك.",
"confirmations.mute.message": "هل أنت متأكد أنك تريد كتم {name} ؟", "confirmations.mute.message": "هل أنت متأكد أنك تريد كتم {name} ؟",
"confirmations.redraft.confirm": "إزالة و إعادة الصياغة", "confirmations.redraft.confirm": "إزالة و إعادة الصياغة",
"confirmations.redraft.message": "هل أنت متأكد من أنك تريد حذف هذا المنشور و إعادة صياغته؟ سوف تفقد جميع الإعجابات و الترقيات أما الردود المتصلة به فستُصبِح يتيمة.", "confirmations.redraft.message": "هل أنت متأكد من أنك تريد حذف هذا المنشور و إعادة صياغته؟ سوف تفقد جميع الإعجابات و الترقيات أما الردود المتصلة به فستُصبِح يتيمة.",
@ -160,7 +167,7 @@
"empty_column.mutes": "لم تقم بكتم أي مستخدم بعد.", "empty_column.mutes": "لم تقم بكتم أي مستخدم بعد.",
"empty_column.notifications": "لم تتلق أي إشعار بعدُ. تفاعل مع المستخدمين الآخرين لإنشاء محادثة.", "empty_column.notifications": "لم تتلق أي إشعار بعدُ. تفاعل مع المستخدمين الآخرين لإنشاء محادثة.",
"empty_column.public": "لا يوجد أي شيء هنا! قم بنشر شيء ما للعامة، أو اتبع المستخدمين الآخرين المتواجدين على الخوادم الأخرى لملء خيط المحادثات", "empty_column.public": "لا يوجد أي شيء هنا! قم بنشر شيء ما للعامة، أو اتبع المستخدمين الآخرين المتواجدين على الخوادم الأخرى لملء خيط المحادثات",
"error.unexpected_crash.explanation": "Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.", "error.unexpected_crash.explanation": "نظرا لوجود خطأ في التعليمات البرمجية أو مشكلة توافق مع المتصفّح، تعذر عرض هذه الصفحة بشكل صحيح.",
"error.unexpected_crash.next_steps": "حاول إعادة إنعاش الصفحة. إن لم تُحلّ المشكلة ، يمكنك دائمًا استخدام ماستدون عبر متصفّح آخر أو تطبيق أصلي.", "error.unexpected_crash.next_steps": "حاول إعادة إنعاش الصفحة. إن لم تُحلّ المشكلة ، يمكنك دائمًا استخدام ماستدون عبر متصفّح آخر أو تطبيق أصلي.",
"errors.unexpected_crash.copy_stacktrace": "انسخ تتبع الارتباطات إلى الحافظة", "errors.unexpected_crash.copy_stacktrace": "انسخ تتبع الارتباطات إلى الحافظة",
"errors.unexpected_crash.report_issue": "الإبلاغ عن خلل", "errors.unexpected_crash.report_issue": "الإبلاغ عن خلل",
@ -236,6 +243,7 @@
"keyboard_shortcuts.reply": "للردّ", "keyboard_shortcuts.reply": "للردّ",
"keyboard_shortcuts.requests": "لفتح قائمة طلبات المتابعة", "keyboard_shortcuts.requests": "لفتح قائمة طلبات المتابعة",
"keyboard_shortcuts.search": "للتركيز على البحث", "keyboard_shortcuts.search": "للتركيز على البحث",
"keyboard_shortcuts.spoilers": "لإظهار/إخفاء حقلCW",
"keyboard_shortcuts.start": "لفتح عمود \"هيا نبدأ\"", "keyboard_shortcuts.start": "لفتح عمود \"هيا نبدأ\"",
"keyboard_shortcuts.toggle_hidden": "لعرض أو إخفاء النص مِن وراء التحذير", "keyboard_shortcuts.toggle_hidden": "لعرض أو إخفاء النص مِن وراء التحذير",
"keyboard_shortcuts.toggle_sensitivity": "لعرض/إخفاء الوسائط", "keyboard_shortcuts.toggle_sensitivity": "لعرض/إخفاء الوسائط",
@ -412,6 +420,10 @@
"time_remaining.minutes": "{number, plural, one {# دقيقة} other {# دقائق}} متبقية", "time_remaining.minutes": "{number, plural, one {# دقيقة} other {# دقائق}} متبقية",
"time_remaining.moments": "لحظات متبقية", "time_remaining.moments": "لحظات متبقية",
"time_remaining.seconds": "{number, plural, one {# ثانية} other {# ثوانٍ}} متبقية", "time_remaining.seconds": "{number, plural, one {# ثانية} other {# ثوانٍ}} متبقية",
"timeline_hint.remote_resource_not_displayed": "{resource} from other servers are not displayed.",
"timeline_hint.resources.followers": "المتابِعون",
"timeline_hint.resources.follows": "Follows",
"timeline_hint.resources.statuses": "التبويقات القديمة",
"trends.count_by_accounts": "{count} {rawCount, plural, zero {} one {شخص واحد} two {شخصين} few {أشخاص} many {أشخاص} other {أشخاص}} تتحدّث", "trends.count_by_accounts": "{count} {rawCount, plural, zero {} one {شخص واحد} two {شخصين} few {أشخاص} many {أشخاص} other {أشخاص}} تتحدّث",
"trends.trending_now": "المتداولة الآن", "trends.trending_now": "المتداولة الآن",
"ui.beforeunload": "سوف تفقد مسودتك إن تركت ماستدون.", "ui.beforeunload": "سوف تفقد مسودتك إن تركت ماستدون.",

View File

@ -1,10 +1,13 @@
{ {
"account.add_or_remove_from_list": "Add or Remove from lists", "account.account_note_header": "Your note for @{name}",
"account.add_account_note": "Add note for @{name}",
"account.add_or_remove_from_list": "Amestar o desaniciar de les llistes",
"account.badges.bot": "Robó", "account.badges.bot": "Robó",
"account.badges.group": "Grupu", "account.badges.group": "Grupu",
"account.block": "Bloquiar a @{name}", "account.block": "Bloquiar a @{name}",
"account.block_domain": "Anubrir tolo de {domain}", "account.block_domain": "Anubrir tolo de {domain}",
"account.blocked": "Blocked", "account.blocked": "Bloquiada",
"account.browse_more_on_origin_server": "Browse more on the original profile",
"account.cancel_follow_request": "Encaboxar la solicitú de siguimientu", "account.cancel_follow_request": "Encaboxar la solicitú de siguimientu",
"account.direct": "Unviar un mensaxe direutu a @{name}", "account.direct": "Unviar un mensaxe direutu a @{name}",
"account.domain_blocked": "Dominiu anubríu", "account.domain_blocked": "Dominiu anubríu",
@ -13,12 +16,12 @@
"account.follow": "Siguir", "account.follow": "Siguir",
"account.followers": "Siguidores", "account.followers": "Siguidores",
"account.followers.empty": "Naide sigue a esti usuariu entá.", "account.followers.empty": "Naide sigue a esti usuariu entá.",
"account.follows": "Follows", "account.follows": "Sigue",
"account.follows.empty": "Esti usuariu entá nun sigue a naide.", "account.follows.empty": "Esti usuariu entá nun sigue a naide.",
"account.follows_you": "Síguete", "account.follows_you": "Síguete",
"account.hide_reblogs": "Anubrir les comparticiones de @{name}", "account.hide_reblogs": "Anubrir les comparticiones de @{name}",
"account.last_status": "Last active", "account.last_status": "Cabera actividá",
"account.link_verified_on": "Ownership of this link was checked on {date}", "account.link_verified_on": "La propiedá d'esti enllaz foi comprobada'l {date}",
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
"account.media": "Media", "account.media": "Media",
"account.mention": "Mentar a @{name}", "account.mention": "Mentar a @{name}",
@ -39,6 +42,10 @@
"account.unfollow": "Dexar de siguir", "account.unfollow": "Dexar de siguir",
"account.unmute": "Unmute @{name}", "account.unmute": "Unmute @{name}",
"account.unmute_notifications": "Unmute notifications from @{name}", "account.unmute_notifications": "Unmute notifications from @{name}",
"account_note.cancel": "Cancel",
"account_note.edit": "Edit",
"account_note.placeholder": "No comment provided",
"account_note.save": "Save",
"alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.",
"alert.rate_limited.title": "Rate limited", "alert.rate_limited.title": "Rate limited",
"alert.unexpected.message": "Asocedió un fallu inesperáu.", "alert.unexpected.message": "Asocedió un fallu inesperáu.",
@ -236,6 +243,7 @@
"keyboard_shortcuts.reply": "pa responder", "keyboard_shortcuts.reply": "pa responder",
"keyboard_shortcuts.requests": "p'abrir la llista de solicitúes de siguimientu", "keyboard_shortcuts.requests": "p'abrir la llista de solicitúes de siguimientu",
"keyboard_shortcuts.search": "pa enfocar la gueta", "keyboard_shortcuts.search": "pa enfocar la gueta",
"keyboard_shortcuts.spoilers": "to show/hide CW field",
"keyboard_shortcuts.start": "p'abrir la columna «entamar»", "keyboard_shortcuts.start": "p'abrir la columna «entamar»",
"keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toggle_sensitivity": "to show/hide media", "keyboard_shortcuts.toggle_sensitivity": "to show/hide media",
@ -412,11 +420,15 @@
"time_remaining.minutes": "{number, plural, one {# minutu restante} other {# minutos restantes}}", "time_remaining.minutes": "{number, plural, one {# minutu restante} other {# minutos restantes}}",
"time_remaining.moments": "Moments remaining", "time_remaining.moments": "Moments remaining",
"time_remaining.seconds": "{number, plural, one {# segundu restante} other {# segundos restantes}}", "time_remaining.seconds": "{number, plural, one {# segundu restante} other {# segundos restantes}}",
"timeline_hint.remote_resource_not_displayed": "{resource} from other servers are not displayed.",
"timeline_hint.resources.followers": "Followers",
"timeline_hint.resources.follows": "Follows",
"timeline_hint.resources.statuses": "Older toots",
"trends.count_by_accounts": "{count} {rawCount, plural, one {persona} other {persones}} falando", "trends.count_by_accounts": "{count} {rawCount, plural, one {persona} other {persones}} falando",
"trends.trending_now": "Trending now", "trends.trending_now": "Trending now",
"ui.beforeunload": "El borrador va perdese si coles de Mastodon.", "ui.beforeunload": "El borrador va perdese si coles de Mastodon.",
"upload_area.title": "Arrastra y suelta pa xubir", "upload_area.title": "Arrastra y suelta pa xubir",
"upload_button.label": "Add media ({formats})", "upload_button.label": "Add images, a video or an audio file",
"upload_error.limit": "File upload limit exceeded.", "upload_error.limit": "File upload limit exceeded.",
"upload_error.poll": "La xuba de ficheros nun ta permitida con encuestes.", "upload_error.poll": "La xuba de ficheros nun ta permitida con encuestes.",
"upload_form.audio_description": "Descripción pa persones con perda auditiva", "upload_form.audio_description": "Descripción pa persones con perda auditiva",

View File

@ -1,10 +1,13 @@
{ {
"account.account_note_header": "Your note for @{name}",
"account.add_account_note": "Add note for @{name}",
"account.add_or_remove_from_list": "Добави или премахни от списъците", "account.add_or_remove_from_list": "Добави или премахни от списъците",
"account.badges.bot": "бот", "account.badges.bot": "бот",
"account.badges.group": "Group", "account.badges.group": "Group",
"account.block": "Блокирай", "account.block": "Блокирай",
"account.block_domain": "скрий всичко от (домейн)", "account.block_domain": "скрий всичко от (домейн)",
"account.blocked": "Блокирани", "account.blocked": "Блокирани",
"account.browse_more_on_origin_server": "Browse more on the original profile",
"account.cancel_follow_request": "Откажи искането за следване", "account.cancel_follow_request": "Откажи искането за следване",
"account.direct": "Direct Message @{name}", "account.direct": "Direct Message @{name}",
"account.domain_blocked": "Скрит домейн", "account.domain_blocked": "Скрит домейн",
@ -39,6 +42,10 @@
"account.unfollow": "Не следвай", "account.unfollow": "Не следвай",
"account.unmute": "Unmute @{name}", "account.unmute": "Unmute @{name}",
"account.unmute_notifications": "Unmute notifications from @{name}", "account.unmute_notifications": "Unmute notifications from @{name}",
"account_note.cancel": "Cancel",
"account_note.edit": "Edit",
"account_note.placeholder": "No comment provided",
"account_note.save": "Save",
"alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.",
"alert.rate_limited.title": "Rate limited", "alert.rate_limited.title": "Rate limited",
"alert.unexpected.message": "An unexpected error occurred.", "alert.unexpected.message": "An unexpected error occurred.",
@ -236,6 +243,7 @@
"keyboard_shortcuts.reply": "to reply", "keyboard_shortcuts.reply": "to reply",
"keyboard_shortcuts.requests": "to open follow requests list", "keyboard_shortcuts.requests": "to open follow requests list",
"keyboard_shortcuts.search": "to focus search", "keyboard_shortcuts.search": "to focus search",
"keyboard_shortcuts.spoilers": "to show/hide CW field",
"keyboard_shortcuts.start": "to open \"get started\" column", "keyboard_shortcuts.start": "to open \"get started\" column",
"keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toggle_sensitivity": "to show/hide media", "keyboard_shortcuts.toggle_sensitivity": "to show/hide media",
@ -412,6 +420,10 @@
"time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
"time_remaining.moments": "Moments remaining", "time_remaining.moments": "Moments remaining",
"time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
"timeline_hint.remote_resource_not_displayed": "{resource} from other servers are not displayed.",
"timeline_hint.resources.followers": "Followers",
"timeline_hint.resources.follows": "Follows",
"timeline_hint.resources.statuses": "Older toots",
"trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
"trends.trending_now": "Trending now", "trends.trending_now": "Trending now",
"ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",

View File

@ -1,10 +1,13 @@
{ {
"account.account_note_header": "Your note for @{name}",
"account.add_account_note": "Add note for @{name}",
"account.add_or_remove_from_list": "তালিকাতে যুক্ত বা অপসারণ করুন", "account.add_or_remove_from_list": "তালিকাতে যুক্ত বা অপসারণ করুন",
"account.badges.bot": "বট", "account.badges.bot": "বট",
"account.badges.group": "Group", "account.badges.group": "Group",
"account.block": "@{name} কে ব্লক করুন", "account.block": "@{name} কে ব্লক করুন",
"account.block_domain": "{domain} থেকে সব আড়াল করুন", "account.block_domain": "{domain} থেকে সব আড়াল করুন",
"account.blocked": "অবরুদ্ধ", "account.blocked": "অবরুদ্ধ",
"account.browse_more_on_origin_server": "Browse more on the original profile",
"account.cancel_follow_request": "অনুসরণ অনুরোধ বাতিল করুন", "account.cancel_follow_request": "অনুসরণ অনুরোধ বাতিল করুন",
"account.direct": "@{name} কে সরাসরি বার্তা", "account.direct": "@{name} কে সরাসরি বার্তা",
"account.domain_blocked": "ডোমেন গোপন করুন", "account.domain_blocked": "ডোমেন গোপন করুন",
@ -39,6 +42,10 @@
"account.unfollow": "অনুসরণ না করতে", "account.unfollow": "অনুসরণ না করতে",
"account.unmute": "@{name} র কার্যকলাপ আবার দেখুন", "account.unmute": "@{name} র কার্যকলাপ আবার দেখুন",
"account.unmute_notifications": "@{name} র প্রজ্ঞাপন দেখুন", "account.unmute_notifications": "@{name} র প্রজ্ঞাপন দেখুন",
"account_note.cancel": "Cancel",
"account_note.edit": "Edit",
"account_note.placeholder": "No comment provided",
"account_note.save": "Save",
"alert.rate_limited.message": "{retry_time, time, medium} -এর পরে আবার প্রচেষ্টা করুন।", "alert.rate_limited.message": "{retry_time, time, medium} -এর পরে আবার প্রচেষ্টা করুন।",
"alert.rate_limited.title": "হার সীমিত", "alert.rate_limited.title": "হার সীমিত",
"alert.unexpected.message": "সমস্যা অপ্রত্যাশিত.", "alert.unexpected.message": "সমস্যা অপ্রত্যাশিত.",
@ -236,6 +243,7 @@
"keyboard_shortcuts.reply": "মতামত দিতে", "keyboard_shortcuts.reply": "মতামত দিতে",
"keyboard_shortcuts.requests": "অনুসরণ অনুরোধের তালিকা দেখতে", "keyboard_shortcuts.requests": "অনুসরণ অনুরোধের তালিকা দেখতে",
"keyboard_shortcuts.search": "খোঁজার অংশে ফোকাস করতে", "keyboard_shortcuts.search": "খোঁজার অংশে ফোকাস করতে",
"keyboard_shortcuts.spoilers": "to show/hide CW field",
"keyboard_shortcuts.start": "\"প্রথম শুরুর\" কলাম বের করতে", "keyboard_shortcuts.start": "\"প্রথম শুরুর\" কলাম বের করতে",
"keyboard_shortcuts.toggle_hidden": "CW লেখা দেখতে বা লুকাতে", "keyboard_shortcuts.toggle_hidden": "CW লেখা দেখতে বা লুকাতে",
"keyboard_shortcuts.toggle_sensitivity": "ভিডিও/ছবি দেখতে বা বন্ধ করতে", "keyboard_shortcuts.toggle_sensitivity": "ভিডিও/ছবি দেখতে বা বন্ধ করতে",
@ -412,6 +420,10 @@
"time_remaining.minutes": "{number, plural, one {# মিনিট} other {# মিনিট}} বাকি আছে", "time_remaining.minutes": "{number, plural, one {# মিনিট} other {# মিনিট}} বাকি আছে",
"time_remaining.moments": "সময় বাকি আছে", "time_remaining.moments": "সময় বাকি আছে",
"time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} বাকি আছে", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} বাকি আছে",
"timeline_hint.remote_resource_not_displayed": "{resource} from other servers are not displayed.",
"timeline_hint.resources.followers": "Followers",
"timeline_hint.resources.follows": "Follows",
"timeline_hint.resources.statuses": "Older toots",
"trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} কথা বলছে", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} কথা বলছে",
"trends.trending_now": "বর্তমানে জনপ্রিয়", "trends.trending_now": "বর্তমানে জনপ্রিয়",
"ui.beforeunload": "যে পর্যন্ত এটা লেখা হয়েছে, মাস্টাডন থেকে চলে গেলে এটা মুছে যাবে।", "ui.beforeunload": "যে পর্যন্ত এটা লেখা হয়েছে, মাস্টাডন থেকে চলে গেলে এটা মুছে যাবে।",

View File

@ -1,10 +1,13 @@
{ {
"account.account_note_header": "Your note for @{name}",
"account.add_account_note": "Add note for @{name}",
"account.add_or_remove_from_list": "Ouzhpenn pe dilemel eus al listennadoù", "account.add_or_remove_from_list": "Ouzhpenn pe dilemel eus al listennadoù",
"account.badges.bot": "Robot", "account.badges.bot": "Robot",
"account.badges.group": "Strollad", "account.badges.group": "Strollad",
"account.block": "Berzañ @{name}", "account.block": "Berzañ @{name}",
"account.block_domain": "Berzañ pep tra eus {domain}", "account.block_domain": "Berzañ pep tra eus {domain}",
"account.blocked": "Stanket", "account.blocked": "Stanket",
"account.browse_more_on_origin_server": "Browse more on the original profile",
"account.cancel_follow_request": "Nullañ ar bedadenn heuliañ", "account.cancel_follow_request": "Nullañ ar bedadenn heuliañ",
"account.direct": "Kas ur gemennadenn da @{name}", "account.direct": "Kas ur gemennadenn da @{name}",
"account.domain_blocked": "Domani berzet", "account.domain_blocked": "Domani berzet",
@ -39,6 +42,10 @@
"account.unfollow": "Diheuliañ", "account.unfollow": "Diheuliañ",
"account.unmute": "Diguzhat @{name}", "account.unmute": "Diguzhat @{name}",
"account.unmute_notifications": "Diguzhat kemennoù a @{name}", "account.unmute_notifications": "Diguzhat kemennoù a @{name}",
"account_note.cancel": "Cancel",
"account_note.edit": "Edit",
"account_note.placeholder": "No comment provided",
"account_note.save": "Save",
"alert.rate_limited.message": "Klaskit en-dro a-benn {retry_time, time, medium}.", "alert.rate_limited.message": "Klaskit en-dro a-benn {retry_time, time, medium}.",
"alert.rate_limited.title": "Feur bevennet", "alert.rate_limited.title": "Feur bevennet",
"alert.unexpected.message": "Ur fazi dic'hortozet zo degouezhet.", "alert.unexpected.message": "Ur fazi dic'hortozet zo degouezhet.",
@ -236,6 +243,7 @@
"keyboard_shortcuts.reply": "da respont", "keyboard_shortcuts.reply": "da respont",
"keyboard_shortcuts.requests": "to open follow requests list", "keyboard_shortcuts.requests": "to open follow requests list",
"keyboard_shortcuts.search": "to focus search", "keyboard_shortcuts.search": "to focus search",
"keyboard_shortcuts.spoilers": "to show/hide CW field",
"keyboard_shortcuts.start": "to open \"get started\" column", "keyboard_shortcuts.start": "to open \"get started\" column",
"keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toggle_sensitivity": "to show/hide media", "keyboard_shortcuts.toggle_sensitivity": "to show/hide media",
@ -257,7 +265,7 @@
"lists.subheading": "Ho listennoù", "lists.subheading": "Ho listennoù",
"load_pending": "{count, plural, one {# new item} other {# new items}}", "load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "O kargañ...", "loading_indicator.label": "O kargañ...",
"media_gallery.toggle_visible": "Hide {number, plural, one {image} other {images}}", "media_gallery.toggle_visible": "Toggle visibility",
"missing_indicator.label": "Digavet", "missing_indicator.label": "Digavet",
"missing_indicator.sublabel": "This resource could not be found", "missing_indicator.sublabel": "This resource could not be found",
"mute_modal.hide_notifications": "Hide notifications from this user?", "mute_modal.hide_notifications": "Hide notifications from this user?",
@ -412,6 +420,10 @@
"time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
"time_remaining.moments": "Moments remaining", "time_remaining.moments": "Moments remaining",
"time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
"timeline_hint.remote_resource_not_displayed": "{resource} from other servers are not displayed.",
"timeline_hint.resources.followers": "Followers",
"timeline_hint.resources.follows": "Follows",
"timeline_hint.resources.statuses": "Older toots",
"trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
"trends.trending_now": "Luskad ar mare", "trends.trending_now": "Luskad ar mare",
"ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",

View File

@ -1,10 +1,13 @@
{ {
"account.account_note_header": "Your note for @{name}",
"account.add_account_note": "Add note for @{name}",
"account.add_or_remove_from_list": "Afegir o Treure de les llistes", "account.add_or_remove_from_list": "Afegir o Treure de les llistes",
"account.badges.bot": "Bot", "account.badges.bot": "Bot",
"account.badges.group": "Grup", "account.badges.group": "Grup",
"account.block": "Bloqueja @{name}", "account.block": "Bloqueja @{name}",
"account.block_domain": "Amaga-ho tot de {domain}", "account.block_domain": "Amaga-ho tot de {domain}",
"account.blocked": "Bloquejat", "account.blocked": "Bloquejat",
"account.browse_more_on_origin_server": "Navega més en el perfil original",
"account.cancel_follow_request": "Anul·la la sol·licitud de seguiment", "account.cancel_follow_request": "Anul·la la sol·licitud de seguiment",
"account.direct": "Missatge directe @{name}", "account.direct": "Missatge directe @{name}",
"account.domain_blocked": "Domini ocult", "account.domain_blocked": "Domini ocult",
@ -39,6 +42,10 @@
"account.unfollow": "Deixa de seguir", "account.unfollow": "Deixa de seguir",
"account.unmute": "Treure silenci de @{name}", "account.unmute": "Treure silenci de @{name}",
"account.unmute_notifications": "Activar notificacions de @{name}", "account.unmute_notifications": "Activar notificacions de @{name}",
"account_note.cancel": "Cancel",
"account_note.edit": "Edit",
"account_note.placeholder": "No comment provided",
"account_note.save": "Save",
"alert.rate_limited.message": "Si us plau torna-ho a provar després de {retry_time, time, medium}.", "alert.rate_limited.message": "Si us plau torna-ho a provar després de {retry_time, time, medium}.",
"alert.rate_limited.title": "Límit de freqüència", "alert.rate_limited.title": "Límit de freqüència",
"alert.unexpected.message": "S'ha produït un error inesperat.", "alert.unexpected.message": "S'ha produït un error inesperat.",
@ -74,9 +81,9 @@
"column_header.show_settings": "Mostra la configuració", "column_header.show_settings": "Mostra la configuració",
"column_header.unpin": "No fixis", "column_header.unpin": "No fixis",
"column_subheading.settings": "Configuració", "column_subheading.settings": "Configuració",
"community.column_settings.local_only": "Local only", "community.column_settings.local_only": "Només local",
"community.column_settings.media_only": "Només multimèdia", "community.column_settings.media_only": "Només multimèdia",
"community.column_settings.remote_only": "Remote only", "community.column_settings.remote_only": "Només remot",
"compose_form.direct_message_warning": "Aquest tut només serà enviat als usuaris esmentats.", "compose_form.direct_message_warning": "Aquest tut només serà enviat als usuaris esmentats.",
"compose_form.direct_message_warning_learn_more": "Aprèn més", "compose_form.direct_message_warning_learn_more": "Aprèn més",
"compose_form.hashtag_warning": "Aquesta tut no es mostrarà en cap etiqueta ja que no està llistat. Només els tuts públics poden ser cercats per etiqueta.", "compose_form.hashtag_warning": "Aquesta tut no es mostrarà en cap etiqueta ja que no està llistat. Només els tuts públics poden ser cercats per etiqueta.",
@ -236,6 +243,7 @@
"keyboard_shortcuts.reply": "respondre", "keyboard_shortcuts.reply": "respondre",
"keyboard_shortcuts.requests": "per a obrir la llista de sol·licituds de seguiment", "keyboard_shortcuts.requests": "per a obrir la llista de sol·licituds de seguiment",
"keyboard_shortcuts.search": "per a centrar la cerca", "keyboard_shortcuts.search": "per a centrar la cerca",
"keyboard_shortcuts.spoilers": "mostrar/amagar el camp CW",
"keyboard_shortcuts.start": "per a obrir la columna \"Començar\"", "keyboard_shortcuts.start": "per a obrir la columna \"Començar\"",
"keyboard_shortcuts.toggle_hidden": "per a mostrar o amagar text sota CW", "keyboard_shortcuts.toggle_hidden": "per a mostrar o amagar text sota CW",
"keyboard_shortcuts.toggle_sensitivity": "per a mostrar o amagar contingut multimèdia", "keyboard_shortcuts.toggle_sensitivity": "per a mostrar o amagar contingut multimèdia",
@ -348,7 +356,7 @@
"report.target": "Informes {target}", "report.target": "Informes {target}",
"search.placeholder": "Cercar", "search.placeholder": "Cercar",
"search_popout.search_format": "Format de cerca avançada", "search_popout.search_format": "Format de cerca avançada",
"search_popout.tips.full_text": "Text simple recupera publicacions que has escrit, les marcades com a favorites, les impulsades o en les que has estat esmentat, així com usuaris, noms d'usuari i etiquetes.", "search_popout.tips.full_text": "Text simple recupera publicacions que has escrit, les marcades com a preferides, les impulsades o en les que has estat esmentat, així com usuaris, noms d'usuari i etiquetes.",
"search_popout.tips.hashtag": "etiqueta", "search_popout.tips.hashtag": "etiqueta",
"search_popout.tips.status": "tut", "search_popout.tips.status": "tut",
"search_popout.tips.text": "El text simple retorna coincidències amb els noms de visualització, els noms d'usuari i les etiquetes", "search_popout.tips.text": "El text simple retorna coincidències amb els noms de visualització, els noms d'usuari i les etiquetes",
@ -364,7 +372,7 @@
"status.bookmark": "Marcador", "status.bookmark": "Marcador",
"status.cancel_reblog_private": "Desfer l'impuls", "status.cancel_reblog_private": "Desfer l'impuls",
"status.cannot_reblog": "Aquesta publicació no pot ser impulsada", "status.cannot_reblog": "Aquesta publicació no pot ser impulsada",
"status.copy": "Copia l'enllaç al tut", "status.copy": "Copia l'enllaç a l'estat",
"status.delete": "Esborrar", "status.delete": "Esborrar",
"status.detailed_status": "Visualització detallada de la conversa", "status.detailed_status": "Visualització detallada de la conversa",
"status.direct": "Missatge directe @{name}", "status.direct": "Missatge directe @{name}",
@ -412,6 +420,10 @@
"time_remaining.minutes": "{number, plural, one {# minut} other {# minuts}} restants", "time_remaining.minutes": "{number, plural, one {# minut} other {# minuts}} restants",
"time_remaining.moments": "Moments restants", "time_remaining.moments": "Moments restants",
"time_remaining.seconds": "{number, plural, one {# segon} other {# segons}} restants", "time_remaining.seconds": "{number, plural, one {# segon} other {# segons}} restants",
"timeline_hint.remote_resource_not_displayed": "{resource} dels altres servidors no son mostrats.",
"timeline_hint.resources.followers": "Seguidors",
"timeline_hint.resources.follows": "Seguiments",
"timeline_hint.resources.statuses": "Tuts més antics",
"trends.count_by_accounts": "{count} {rawCount, plural, one {persona} other {persones}} parlant-hi", "trends.count_by_accounts": "{count} {rawCount, plural, one {persona} other {persones}} parlant-hi",
"trends.trending_now": "Ara en tendència", "trends.trending_now": "Ara en tendència",
"ui.beforeunload": "El teu esborrany es perdrà si surts de Mastodon.", "ui.beforeunload": "El teu esborrany es perdrà si surts de Mastodon.",

View File

@ -1,10 +1,13 @@
{ {
"account.account_note_header": "Your note for @{name}",
"account.add_account_note": "Add note for @{name}",
"account.add_or_remove_from_list": "Aghjunghje o toglie da e liste", "account.add_or_remove_from_list": "Aghjunghje o toglie da e liste",
"account.badges.bot": "Bot", "account.badges.bot": "Bot",
"account.badges.group": "Gruppu", "account.badges.group": "Gruppu",
"account.block": "Bluccà @{name}", "account.block": "Bluccà @{name}",
"account.block_domain": "Piattà u duminiu {domain}", "account.block_domain": "Piattà u duminiu {domain}",
"account.blocked": "Bluccatu", "account.blocked": "Bluccatu",
"account.browse_more_on_origin_server": "Vede di più nant'à u prufile uriginale",
"account.cancel_follow_request": "Annullà a dumanda d'abbunamentu", "account.cancel_follow_request": "Annullà a dumanda d'abbunamentu",
"account.direct": "Missaghju direttu @{name}", "account.direct": "Missaghju direttu @{name}",
"account.domain_blocked": "Duminiu piattatu", "account.domain_blocked": "Duminiu piattatu",
@ -39,6 +42,10 @@
"account.unfollow": "Ùn siguità più", "account.unfollow": "Ùn siguità più",
"account.unmute": "Ùn piattà più @{name}", "account.unmute": "Ùn piattà più @{name}",
"account.unmute_notifications": "Ùn piattà più nutificazione da @{name}", "account.unmute_notifications": "Ùn piattà più nutificazione da @{name}",
"account_note.cancel": "Cancel",
"account_note.edit": "Edit",
"account_note.placeholder": "No comment provided",
"account_note.save": "Save",
"alert.rate_limited.message": "Pruvate ancu dop'à {retry_time, time, medium}.", "alert.rate_limited.message": "Pruvate ancu dop'à {retry_time, time, medium}.",
"alert.rate_limited.title": "Ghjettu limitatu", "alert.rate_limited.title": "Ghjettu limitatu",
"alert.unexpected.message": "Un prublemu inaspettatu hè accadutu.", "alert.unexpected.message": "Un prublemu inaspettatu hè accadutu.",
@ -74,9 +81,9 @@
"column_header.show_settings": "Mustrà i parametri", "column_header.show_settings": "Mustrà i parametri",
"column_header.unpin": "Spuntarulà", "column_header.unpin": "Spuntarulà",
"column_subheading.settings": "Parametri", "column_subheading.settings": "Parametri",
"community.column_settings.local_only": "Local only", "community.column_settings.local_only": "Solu lucale",
"community.column_settings.media_only": "Solu media", "community.column_settings.media_only": "Solu media",
"community.column_settings.remote_only": "Remote only", "community.column_settings.remote_only": "Solu distante",
"compose_form.direct_message_warning": "Solu l'utilizatori mintuvati puderenu vede stu statutu.", "compose_form.direct_message_warning": "Solu l'utilizatori mintuvati puderenu vede stu statutu.",
"compose_form.direct_message_warning_learn_more": "Amparà di più", "compose_form.direct_message_warning_learn_more": "Amparà di più",
"compose_form.hashtag_warning": "Stu statutu ùn hè \"Micca listatu\" è ùn sarà micca listatu indè e circate da hashtag. Per esse vistu in quesse, u statutu deve esse \"Pubblicu\".", "compose_form.hashtag_warning": "Stu statutu ùn hè \"Micca listatu\" è ùn sarà micca listatu indè e circate da hashtag. Per esse vistu in quesse, u statutu deve esse \"Pubblicu\".",
@ -236,6 +243,7 @@
"keyboard_shortcuts.reply": "risponde", "keyboard_shortcuts.reply": "risponde",
"keyboard_shortcuts.requests": "per apre a lista di dumande d'abbunamentu", "keyboard_shortcuts.requests": "per apre a lista di dumande d'abbunamentu",
"keyboard_shortcuts.search": "fucalizà nant'à l'area di circata", "keyboard_shortcuts.search": "fucalizà nant'à l'area di circata",
"keyboard_shortcuts.spoilers": "per mustrà/piattà u campu CW",
"keyboard_shortcuts.start": "per apre a culonna \"per principià\"", "keyboard_shortcuts.start": "per apre a culonna \"per principià\"",
"keyboard_shortcuts.toggle_hidden": "vede/piattà u testu daretu à l'avertimentu CW", "keyboard_shortcuts.toggle_hidden": "vede/piattà u testu daretu à l'avertimentu CW",
"keyboard_shortcuts.toggle_sensitivity": "vede/piattà i media", "keyboard_shortcuts.toggle_sensitivity": "vede/piattà i media",
@ -412,6 +420,10 @@
"time_remaining.minutes": "{number, plural, one {# minuta ferma} other {# minute fermanu}} left", "time_remaining.minutes": "{number, plural, one {# minuta ferma} other {# minute fermanu}} left",
"time_remaining.moments": "Ci fermanu qualchi mumentu", "time_remaining.moments": "Ci fermanu qualchi mumentu",
"time_remaining.seconds": "{number, plural, one {# siconda ferma} other {# siconde fermanu}}", "time_remaining.seconds": "{number, plural, one {# siconda ferma} other {# siconde fermanu}}",
"timeline_hint.remote_resource_not_displayed": "{resource} da l'altri servori ùn so micca affissati·e.",
"timeline_hint.resources.followers": "Abbunati",
"timeline_hint.resources.follows": "Abbunamenti",
"timeline_hint.resources.statuses": "Statuti più anziani",
"trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} parlanu", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} parlanu",
"trends.trending_now": "Tindenze d'avà", "trends.trending_now": "Tindenze d'avà",
"ui.beforeunload": "A bruttacopia sarà persa s'ellu hè chjosu Mastodon.", "ui.beforeunload": "A bruttacopia sarà persa s'ellu hè chjosu Mastodon.",

View File

@ -1,10 +1,13 @@
{ {
"account.account_note_header": "Your note for @{name}",
"account.add_account_note": "Add note for @{name}",
"account.add_or_remove_from_list": "Přidat nebo odstranit ze seznamů", "account.add_or_remove_from_list": "Přidat nebo odstranit ze seznamů",
"account.badges.bot": "Robot", "account.badges.bot": "Robot",
"account.badges.group": "Skupina", "account.badges.group": "Skupina",
"account.block": "Zablokovat uživatele @{name}", "account.block": "Zablokovat uživatele @{name}",
"account.block_domain": "Skrýt vše ze serveru {domain}", "account.block_domain": "Skrýt vše ze serveru {domain}",
"account.blocked": "Blokováno", "account.blocked": "Blokováno",
"account.browse_more_on_origin_server": "Více na původním profilu",
"account.cancel_follow_request": "Zrušit žádost o sledování", "account.cancel_follow_request": "Zrušit žádost o sledování",
"account.direct": "Poslat uživateli @{name} přímou zprávu", "account.direct": "Poslat uživateli @{name} přímou zprávu",
"account.domain_blocked": "Doména skryta", "account.domain_blocked": "Doména skryta",
@ -39,6 +42,10 @@
"account.unfollow": "Přestat sledovat", "account.unfollow": "Přestat sledovat",
"account.unmute": "Odkrýt uživatele @{name}", "account.unmute": "Odkrýt uživatele @{name}",
"account.unmute_notifications": "Odkrýt oznámení od uživatele @{name}", "account.unmute_notifications": "Odkrýt oznámení od uživatele @{name}",
"account_note.cancel": "Cancel",
"account_note.edit": "Edit",
"account_note.placeholder": "No comment provided",
"account_note.save": "Save",
"alert.rate_limited.message": "Zkuste to prosím znovu za {retry_time, time, medium}.", "alert.rate_limited.message": "Zkuste to prosím znovu za {retry_time, time, medium}.",
"alert.rate_limited.title": "Rychlost omezena", "alert.rate_limited.title": "Rychlost omezena",
"alert.unexpected.message": "Objevila se neočekávaná chyba.", "alert.unexpected.message": "Objevila se neočekávaná chyba.",
@ -74,9 +81,9 @@
"column_header.show_settings": "Zobrazit nastavení", "column_header.show_settings": "Zobrazit nastavení",
"column_header.unpin": "Odepnout", "column_header.unpin": "Odepnout",
"column_subheading.settings": "Nastavení", "column_subheading.settings": "Nastavení",
"community.column_settings.local_only": "Local only", "community.column_settings.local_only": "Pouze místní",
"community.column_settings.media_only": "Pouze média", "community.column_settings.media_only": "Pouze média",
"community.column_settings.remote_only": "Remote only", "community.column_settings.remote_only": "Pouze vzdálené",
"compose_form.direct_message_warning": "Tento toot bude odeslán pouze zmíněným uživatelům.", "compose_form.direct_message_warning": "Tento toot bude odeslán pouze zmíněným uživatelům.",
"compose_form.direct_message_warning_learn_more": "Zjistit více", "compose_form.direct_message_warning_learn_more": "Zjistit více",
"compose_form.hashtag_warning": "Tento toot nebude zobrazen pod žádným hashtagem, neboť je neuvedený. Pouze veřejné tooty mohou být vyhledány podle hashtagu.", "compose_form.hashtag_warning": "Tento toot nebude zobrazen pod žádným hashtagem, neboť je neuvedený. Pouze veřejné tooty mohou být vyhledány podle hashtagu.",
@ -166,7 +173,7 @@
"errors.unexpected_crash.report_issue": "Nahlásit problém", "errors.unexpected_crash.report_issue": "Nahlásit problém",
"follow_request.authorize": "Autorizovat", "follow_request.authorize": "Autorizovat",
"follow_request.reject": "Odmítnout", "follow_request.reject": "Odmítnout",
"follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.", "follow_requests.unlocked_explanation": "Přestože váš účet není uzamčen, {domain} si myslí, že budete chtít následující požadavky na sledování zkontrolovat ručně.",
"getting_started.developers": "Vývojáři", "getting_started.developers": "Vývojáři",
"getting_started.directory": "Adresář profilů", "getting_started.directory": "Adresář profilů",
"getting_started.documentation": "Dokumentace", "getting_started.documentation": "Dokumentace",
@ -236,6 +243,7 @@
"keyboard_shortcuts.reply": "odpovědět", "keyboard_shortcuts.reply": "odpovědět",
"keyboard_shortcuts.requests": "otevření seznamu požadavků o sledování", "keyboard_shortcuts.requests": "otevření seznamu požadavků o sledování",
"keyboard_shortcuts.search": "zaměření na hledání", "keyboard_shortcuts.search": "zaměření na hledání",
"keyboard_shortcuts.spoilers": "zobrazit/skrýt pole CW",
"keyboard_shortcuts.start": "otevření sloupce „začínáme“", "keyboard_shortcuts.start": "otevření sloupce „začínáme“",
"keyboard_shortcuts.toggle_hidden": "zobrazení/skrytí textu za varováním o obsahu", "keyboard_shortcuts.toggle_hidden": "zobrazení/skrytí textu za varováním o obsahu",
"keyboard_shortcuts.toggle_sensitivity": "zobrazení/skrytí médií", "keyboard_shortcuts.toggle_sensitivity": "zobrazení/skrytí médií",
@ -412,6 +420,10 @@
"time_remaining.minutes": "{number, plural, one {Zbývá # minuta} few {Zbývají # minuty} many {Zbývá # minut} other {Zbývá # minut}}", "time_remaining.minutes": "{number, plural, one {Zbývá # minuta} few {Zbývají # minuty} many {Zbývá # minut} other {Zbývá # minut}}",
"time_remaining.moments": "Zbývá několik sekund", "time_remaining.moments": "Zbývá několik sekund",
"time_remaining.seconds": "{number, plural, one {Zbývá # sekunda} few {Zbývají # sekundy} many {Zbývá # sekund} other {Zbývá # sekund}}", "time_remaining.seconds": "{number, plural, one {Zbývá # sekunda} few {Zbývají # sekundy} many {Zbývá # sekund} other {Zbývá # sekund}}",
"timeline_hint.remote_resource_not_displayed": "{resource} z jiných serveru se nezobrazuje.",
"timeline_hint.resources.followers": "Sledující",
"timeline_hint.resources.follows": "Sleduje",
"timeline_hint.resources.statuses": "Starší tooty",
"trends.count_by_accounts": "{count} {rawCount, plural, one {člověk} few {lidé} many {lidí} other {lidí}} hovoří", "trends.count_by_accounts": "{count} {rawCount, plural, one {člověk} few {lidé} many {lidí} other {lidí}} hovoří",
"trends.trending_now": "Aktuální trendy", "trends.trending_now": "Aktuální trendy",
"ui.beforeunload": "Pokud Mastodon opustíte, váš koncept se ztratí.", "ui.beforeunload": "Pokud Mastodon opustíte, váš koncept se ztratí.",

View File

@ -1,10 +1,13 @@
{ {
"account.account_note_header": "Your note for @{name}",
"account.add_account_note": "Add note for @{name}",
"account.add_or_remove_from_list": "Ychwanegu neu Dileu o'r rhestrau", "account.add_or_remove_from_list": "Ychwanegu neu Dileu o'r rhestrau",
"account.badges.bot": "Bot", "account.badges.bot": "Bot",
"account.badges.group": "Grŵp", "account.badges.group": "Grŵp",
"account.block": "Blocio @{name}", "account.block": "Blocio @{name}",
"account.block_domain": "Cuddio popeth rhag {domain}", "account.block_domain": "Cuddio popeth rhag {domain}",
"account.blocked": "Blociwyd", "account.blocked": "Blociwyd",
"account.browse_more_on_origin_server": "Browse more on the original profile",
"account.cancel_follow_request": "Canslo cais dilyn", "account.cancel_follow_request": "Canslo cais dilyn",
"account.direct": "Neges breifat @{name}", "account.direct": "Neges breifat @{name}",
"account.domain_blocked": "Parth wedi ei guddio", "account.domain_blocked": "Parth wedi ei guddio",
@ -39,6 +42,10 @@
"account.unfollow": "Dad-ddilyn", "account.unfollow": "Dad-ddilyn",
"account.unmute": "Dad-dawelu @{name}", "account.unmute": "Dad-dawelu @{name}",
"account.unmute_notifications": "Dad-dawelu hysbysiadau o @{name}", "account.unmute_notifications": "Dad-dawelu hysbysiadau o @{name}",
"account_note.cancel": "Cancel",
"account_note.edit": "Edit",
"account_note.placeholder": "No comment provided",
"account_note.save": "Save",
"alert.rate_limited.message": "Ceisiwch eto ar ôl {retry_time, time, medium}.", "alert.rate_limited.message": "Ceisiwch eto ar ôl {retry_time, time, medium}.",
"alert.rate_limited.title": "Cyfradd gyfyngedig", "alert.rate_limited.title": "Cyfradd gyfyngedig",
"alert.unexpected.message": "Digwyddodd gwall annisgwyl.", "alert.unexpected.message": "Digwyddodd gwall annisgwyl.",
@ -74,9 +81,9 @@
"column_header.show_settings": "Dangos gosodiadau", "column_header.show_settings": "Dangos gosodiadau",
"column_header.unpin": "Dadbinio", "column_header.unpin": "Dadbinio",
"column_subheading.settings": "Gosodiadau", "column_subheading.settings": "Gosodiadau",
"community.column_settings.local_only": "Local only", "community.column_settings.local_only": "Lleol yn unig",
"community.column_settings.media_only": "Cyfryngau yn unig", "community.column_settings.media_only": "Cyfryngau yn unig",
"community.column_settings.remote_only": "Remote only", "community.column_settings.remote_only": "Anghysbell yn unig",
"compose_form.direct_message_warning": "Mi fydd y tŵt hwn ond yn cael ei anfon at y defnyddwyr sy'n cael eu crybwyll.", "compose_form.direct_message_warning": "Mi fydd y tŵt hwn ond yn cael ei anfon at y defnyddwyr sy'n cael eu crybwyll.",
"compose_form.direct_message_warning_learn_more": "Dysgu mwy", "compose_form.direct_message_warning_learn_more": "Dysgu mwy",
"compose_form.hashtag_warning": "Ni fydd y tŵt hwn wedi ei restru o dan unrhyw hashnod gan ei fod heb ei restru. Dim ond tŵtiau cyhoeddus gellid chwilota amdanynt drwy hashnod.", "compose_form.hashtag_warning": "Ni fydd y tŵt hwn wedi ei restru o dan unrhyw hashnod gan ei fod heb ei restru. Dim ond tŵtiau cyhoeddus gellid chwilota amdanynt drwy hashnod.",
@ -236,6 +243,7 @@
"keyboard_shortcuts.reply": "i ateb", "keyboard_shortcuts.reply": "i ateb",
"keyboard_shortcuts.requests": "i agor rhestr ceisiadau dilyn", "keyboard_shortcuts.requests": "i agor rhestr ceisiadau dilyn",
"keyboard_shortcuts.search": "i ffocysu chwilio", "keyboard_shortcuts.search": "i ffocysu chwilio",
"keyboard_shortcuts.spoilers": "to show/hide CW field",
"keyboard_shortcuts.start": "i agor colofn \"dechrau arni\"", "keyboard_shortcuts.start": "i agor colofn \"dechrau arni\"",
"keyboard_shortcuts.toggle_hidden": "i ddangos/cuddio testun tu ôl i CW", "keyboard_shortcuts.toggle_hidden": "i ddangos/cuddio testun tu ôl i CW",
"keyboard_shortcuts.toggle_sensitivity": "i ddangos/gyddio cyfryngau", "keyboard_shortcuts.toggle_sensitivity": "i ddangos/gyddio cyfryngau",
@ -412,6 +420,10 @@
"time_remaining.minutes": "{number, plural, one {# funud} other {# o funudau}} ar ôl", "time_remaining.minutes": "{number, plural, one {# funud} other {# o funudau}} ar ôl",
"time_remaining.moments": "Munudau ar ôl", "time_remaining.moments": "Munudau ar ôl",
"time_remaining.seconds": "{number, plural, one {# eiliad} other {# o eiliadau}} ar ôl", "time_remaining.seconds": "{number, plural, one {# eiliad} other {# o eiliadau}} ar ôl",
"timeline_hint.remote_resource_not_displayed": "{resource} from other servers are not displayed.",
"timeline_hint.resources.followers": "Followers",
"timeline_hint.resources.follows": "Follows",
"timeline_hint.resources.statuses": "Older toots",
"trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} yn siarad", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} yn siarad",
"trends.trending_now": "Yn tueddu nawr", "trends.trending_now": "Yn tueddu nawr",
"ui.beforeunload": "Mi fyddwch yn colli eich drafft os gadewch Mastodon.", "ui.beforeunload": "Mi fyddwch yn colli eich drafft os gadewch Mastodon.",

View File

@ -1,10 +1,13 @@
{ {
"account.account_note_header": "Your note for @{name}",
"account.add_account_note": "Add note for @{name}",
"account.add_or_remove_from_list": "Tilføj eller fjern fra lister", "account.add_or_remove_from_list": "Tilføj eller fjern fra lister",
"account.badges.bot": "Robot", "account.badges.bot": "Robot",
"account.badges.group": "Gruppe", "account.badges.group": "Gruppe",
"account.block": "Bloker @{name}", "account.block": "Bloker @{name}",
"account.block_domain": "Skjul alt fra {domain}", "account.block_domain": "Skjul alt fra {domain}",
"account.blocked": "Blokeret", "account.blocked": "Blokeret",
"account.browse_more_on_origin_server": "Browse more on the original profile",
"account.cancel_follow_request": "Annullér følgeranmodning", "account.cancel_follow_request": "Annullér følgeranmodning",
"account.direct": "Send en direkte besked til @{name}", "account.direct": "Send en direkte besked til @{name}",
"account.domain_blocked": "Domænet er blevet skjult", "account.domain_blocked": "Domænet er blevet skjult",
@ -39,6 +42,10 @@
"account.unfollow": "Følg ikke længere", "account.unfollow": "Følg ikke længere",
"account.unmute": "Fjern dæmpningen af @{name}", "account.unmute": "Fjern dæmpningen af @{name}",
"account.unmute_notifications": "Fjern dæmpningen af notifikationer fra @{name}", "account.unmute_notifications": "Fjern dæmpningen af notifikationer fra @{name}",
"account_note.cancel": "Cancel",
"account_note.edit": "Edit",
"account_note.placeholder": "No comment provided",
"account_note.save": "Save",
"alert.rate_limited.message": "Prøv venligst igen efter {retry_time, time, medium}.", "alert.rate_limited.message": "Prøv venligst igen efter {retry_time, time, medium}.",
"alert.rate_limited.title": "Gradsbegrænset", "alert.rate_limited.title": "Gradsbegrænset",
"alert.unexpected.message": "Der opstod en uventet fejl.", "alert.unexpected.message": "Der opstod en uventet fejl.",
@ -236,6 +243,7 @@
"keyboard_shortcuts.reply": "for at svare", "keyboard_shortcuts.reply": "for at svare",
"keyboard_shortcuts.requests": "for at åbne listen over følgeranmodninger", "keyboard_shortcuts.requests": "for at åbne listen over følgeranmodninger",
"keyboard_shortcuts.search": "for at fokusere søgningen", "keyboard_shortcuts.search": "for at fokusere søgningen",
"keyboard_shortcuts.spoilers": "to show/hide CW field",
"keyboard_shortcuts.start": "for at åbne \"kom igen\" kolonnen", "keyboard_shortcuts.start": "for at åbne \"kom igen\" kolonnen",
"keyboard_shortcuts.toggle_hidden": "for at vise/skjule tekst bag CW", "keyboard_shortcuts.toggle_hidden": "for at vise/skjule tekst bag CW",
"keyboard_shortcuts.toggle_sensitivity": "for at vise/skjule medier", "keyboard_shortcuts.toggle_sensitivity": "for at vise/skjule medier",
@ -412,6 +420,10 @@
"time_remaining.minutes": "{number, plural, one {# minut} other {# minutter}} tilbage", "time_remaining.minutes": "{number, plural, one {# minut} other {# minutter}} tilbage",
"time_remaining.moments": "Få øjeblikke tilbage", "time_remaining.moments": "Få øjeblikke tilbage",
"time_remaining.seconds": "{number, plural, one {# sekund} other {# sekunder}} tilbage", "time_remaining.seconds": "{number, plural, one {# sekund} other {# sekunder}} tilbage",
"timeline_hint.remote_resource_not_displayed": "{resource} from other servers are not displayed.",
"timeline_hint.resources.followers": "Followers",
"timeline_hint.resources.follows": "Follows",
"timeline_hint.resources.statuses": "Older toots",
"trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {personer}} snakker", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {personer}} snakker",
"trends.trending_now": "Hot lige nu", "trends.trending_now": "Hot lige nu",
"ui.beforeunload": "Din kladde vil gå tabt hvis du forlader Mastodon.", "ui.beforeunload": "Din kladde vil gå tabt hvis du forlader Mastodon.",

View File

@ -1,10 +1,13 @@
{ {
"account.account_note_header": "Your note for @{name}",
"account.add_account_note": "Add note for @{name}",
"account.add_or_remove_from_list": "Hinzufügen oder Entfernen von Listen", "account.add_or_remove_from_list": "Hinzufügen oder Entfernen von Listen",
"account.badges.bot": "Bot", "account.badges.bot": "Bot",
"account.badges.group": "Gruppe", "account.badges.group": "Gruppe",
"account.block": "@{name} blockieren", "account.block": "@{name} blockieren",
"account.block_domain": "Alles von {domain} blockieren", "account.block_domain": "Alles von {domain} blockieren",
"account.blocked": "Blockiert", "account.blocked": "Blockiert",
"account.browse_more_on_origin_server": "Mehr auf dem Originalprofil durchsuchen",
"account.cancel_follow_request": "Folgeanfrage abbrechen", "account.cancel_follow_request": "Folgeanfrage abbrechen",
"account.direct": "Direktnachricht an @{name}", "account.direct": "Direktnachricht an @{name}",
"account.domain_blocked": "Domain versteckt", "account.domain_blocked": "Domain versteckt",
@ -39,6 +42,10 @@
"account.unfollow": "Entfolgen", "account.unfollow": "Entfolgen",
"account.unmute": "@{name} nicht mehr stummschalten", "account.unmute": "@{name} nicht mehr stummschalten",
"account.unmute_notifications": "Benachrichtigungen von @{name} einschalten", "account.unmute_notifications": "Benachrichtigungen von @{name} einschalten",
"account_note.cancel": "Cancel",
"account_note.edit": "Edit",
"account_note.placeholder": "No comment provided",
"account_note.save": "Save",
"alert.rate_limited.message": "Bitte versuche es nach {retry_time, time, medium}.", "alert.rate_limited.message": "Bitte versuche es nach {retry_time, time, medium}.",
"alert.rate_limited.title": "Anfragelimit überschritten", "alert.rate_limited.title": "Anfragelimit überschritten",
"alert.unexpected.message": "Ein unerwarteter Fehler ist aufgetreten.", "alert.unexpected.message": "Ein unerwarteter Fehler ist aufgetreten.",
@ -74,9 +81,9 @@
"column_header.show_settings": "Einstellungen anzeigen", "column_header.show_settings": "Einstellungen anzeigen",
"column_header.unpin": "Lösen", "column_header.unpin": "Lösen",
"column_subheading.settings": "Einstellungen", "column_subheading.settings": "Einstellungen",
"community.column_settings.local_only": "Local only", "community.column_settings.local_only": "Nur lokal",
"community.column_settings.media_only": "Nur Medien", "community.column_settings.media_only": "Nur Medien",
"community.column_settings.remote_only": "Remote only", "community.column_settings.remote_only": "Nur entfernt",
"compose_form.direct_message_warning": "Dieser Beitrag wird nur für die erwähnten Nutzer sichtbar sein.", "compose_form.direct_message_warning": "Dieser Beitrag wird nur für die erwähnten Nutzer sichtbar sein.",
"compose_form.direct_message_warning_learn_more": "Mehr erfahren", "compose_form.direct_message_warning_learn_more": "Mehr erfahren",
"compose_form.hashtag_warning": "Dieser Beitrag wird nicht durch Hashtags entdeckbar sein, weil er ungelistet ist. Nur öffentliche Beiträge tauchen in Hashtag-Zeitleisten auf.", "compose_form.hashtag_warning": "Dieser Beitrag wird nicht durch Hashtags entdeckbar sein, weil er ungelistet ist. Nur öffentliche Beiträge tauchen in Hashtag-Zeitleisten auf.",
@ -113,7 +120,7 @@
"confirmations.mute.explanation": "Dies wird Beiträge von dieser Person und Beiträge, die diese Person erwähnen, ausblenden, aber es wird der Person trotzdem erlauben, deine Beiträge zu sehen und dir zu folgen.", "confirmations.mute.explanation": "Dies wird Beiträge von dieser Person und Beiträge, die diese Person erwähnen, ausblenden, aber es wird der Person trotzdem erlauben, deine Beiträge zu sehen und dir zu folgen.",
"confirmations.mute.message": "Bist du dir sicher, dass du {name} stummschalten möchtest?", "confirmations.mute.message": "Bist du dir sicher, dass du {name} stummschalten möchtest?",
"confirmations.redraft.confirm": "Löschen und neu erstellen", "confirmations.redraft.confirm": "Löschen und neu erstellen",
"confirmations.redraft.message": "Bist du dir sicher, dass du diesen Beitrag löschen und neu erstellen möchtest? Favorisierungen, geteilte Beiträge und Antworten werden verloren gehen.", "confirmations.redraft.message": "Bist du dir sicher, dass du diesen Tröt löschen und neu erstellen möchtest? Favs, geteilte Beiträge und Antworten werden verloren gehen.",
"confirmations.reply.confirm": "Antworten", "confirmations.reply.confirm": "Antworten",
"confirmations.reply.message": "Wenn du jetzt antwortest wird es die gesamte Nachricht verwerfen, die du gerade schreibst. Möchtest du wirklich fortfahren?", "confirmations.reply.message": "Wenn du jetzt antwortest wird es die gesamte Nachricht verwerfen, die du gerade schreibst. Möchtest du wirklich fortfahren?",
"confirmations.unfollow.confirm": "Entfolgen", "confirmations.unfollow.confirm": "Entfolgen",
@ -236,6 +243,7 @@
"keyboard_shortcuts.reply": "antworten", "keyboard_shortcuts.reply": "antworten",
"keyboard_shortcuts.requests": "Liste der Folge-Anfragen öffnen", "keyboard_shortcuts.requests": "Liste der Folge-Anfragen öffnen",
"keyboard_shortcuts.search": "Suche fokussieren", "keyboard_shortcuts.search": "Suche fokussieren",
"keyboard_shortcuts.spoilers": "um CW-Feld anzuzeigen/auszublenden",
"keyboard_shortcuts.start": "\"Erste Schritte\"-Spalte öffnen", "keyboard_shortcuts.start": "\"Erste Schritte\"-Spalte öffnen",
"keyboard_shortcuts.toggle_hidden": "Text hinter einer Inhaltswarnung verstecken/anzeigen", "keyboard_shortcuts.toggle_hidden": "Text hinter einer Inhaltswarnung verstecken/anzeigen",
"keyboard_shortcuts.toggle_sensitivity": "Medien hinter einer Inhaltswarnung verstecken/anzeigen", "keyboard_shortcuts.toggle_sensitivity": "Medien hinter einer Inhaltswarnung verstecken/anzeigen",
@ -348,7 +356,7 @@
"report.target": "{target} melden", "report.target": "{target} melden",
"search.placeholder": "Suche", "search.placeholder": "Suche",
"search_popout.search_format": "Fortgeschrittenes Suchformat", "search_popout.search_format": "Fortgeschrittenes Suchformat",
"search_popout.tips.full_text": "Einfache Texteingabe gibt Beiträge, die du geschrieben, favorisiert und geteilt hast zurück. Außerdem auch Beiträge in denen du erwähnt wurdest, aber auch passende Nutzernamen, Anzeigenamen oder Hashtags.", "search_popout.tips.full_text": "Einfache Texteingabe gibt Tröts, die du geschrieben, gefavt und geteilt hast zurück. Außerdem auch Tröts, in denen du erwähnt wurdest, aber auch passende Nutzernamen, Anzeigenamen, oder Hashtags.",
"search_popout.tips.hashtag": "Hashtag", "search_popout.tips.hashtag": "Hashtag",
"search_popout.tips.status": "Beitrag", "search_popout.tips.status": "Beitrag",
"search_popout.tips.text": "Einfache Texteingabe gibt Anzeigenamen, Benutzernamen und Hashtags zurück", "search_popout.tips.text": "Einfache Texteingabe gibt Anzeigenamen, Benutzernamen und Hashtags zurück",
@ -412,6 +420,10 @@
"time_remaining.minutes": "{number, plural, one {# Minute} other {# Minuten}} verbleibend", "time_remaining.minutes": "{number, plural, one {# Minute} other {# Minuten}} verbleibend",
"time_remaining.moments": "Schließt in Kürze", "time_remaining.moments": "Schließt in Kürze",
"time_remaining.seconds": "{number, plural, one {# Sekunde} other {# Sekunden}} verbleibend", "time_remaining.seconds": "{number, plural, one {# Sekunde} other {# Sekunden}} verbleibend",
"timeline_hint.remote_resource_not_displayed": "{resource} von anderen Servern werden nicht angezeigt.",
"timeline_hint.resources.followers": "Follower",
"timeline_hint.resources.follows": "Folgt",
"timeline_hint.resources.statuses": "Ältere Toots",
"trends.count_by_accounts": "{count} {rawCount, plural, eine {Person} other {Personen}} reden darüber", "trends.count_by_accounts": "{count} {rawCount, plural, eine {Person} other {Personen}} reden darüber",
"trends.trending_now": "In den Trends", "trends.trending_now": "In den Trends",
"ui.beforeunload": "Dein Entwurf geht verloren, wenn du Mastodon verlässt.", "ui.beforeunload": "Dein Entwurf geht verloren, wenn du Mastodon verlässt.",

View File

@ -139,6 +139,23 @@
], ],
"path": "app/javascript/mastodon/components/column_header.json" "path": "app/javascript/mastodon/components/column_header.json"
}, },
{
"descriptors": [
{
"defaultMessage": "{count, plural, one {{counter} Toot} other {{counter} Toots}}",
"id": "account.statuses_counter"
},
{
"defaultMessage": "{count, plural, other {{counter} Following}}",
"id": "account.following_counter"
},
{
"defaultMessage": "{count, plural, one {{counter} Follower} other {{counter} Followers}}",
"id": "account.followers_counter"
}
],
"path": "app/javascript/mastodon/components/common_counter.json"
},
{ {
"descriptors": [ "descriptors": [
{ {
@ -172,8 +189,8 @@
{ {
"descriptors": [ "descriptors": [
{ {
"defaultMessage": "{count} {rawCount, plural, one {person} other {people}} talking", "defaultMessage": "{count, plural, one {{counter} person} other {{counter} people}} talking",
"id": "trends.count_by_accounts" "id": "trends.counter_by_accounts"
} }
], ],
"path": "app/javascript/mastodon/components/hashtag.json" "path": "app/javascript/mastodon/components/hashtag.json"
@ -340,6 +357,23 @@
], ],
"path": "app/javascript/mastodon/components/relative_timestamp.json" "path": "app/javascript/mastodon/components/relative_timestamp.json"
}, },
{
"descriptors": [
{
"defaultMessage": "{count}K",
"id": "units.short.thousand"
},
{
"defaultMessage": "{count}M",
"id": "units.short.million"
},
{
"defaultMessage": "{count}B",
"id": "units.short.billion"
}
],
"path": "app/javascript/mastodon/components/short_number.json"
},
{ {
"descriptors": [ "descriptors": [
{ {
@ -492,6 +526,22 @@
}, },
{ {
"descriptors": [ "descriptors": [
{
"defaultMessage": "Public",
"id": "privacy.public.short"
},
{
"defaultMessage": "Unlisted",
"id": "privacy.unlisted.short"
},
{
"defaultMessage": "Followers-only",
"id": "privacy.private.short"
},
{
"defaultMessage": "Direct",
"id": "privacy.direct.short"
},
{ {
"defaultMessage": "Filtered", "defaultMessage": "Filtered",
"id": "status.filtered" "id": "status.filtered"
@ -507,6 +557,19 @@
], ],
"path": "app/javascript/mastodon/components/status.json" "path": "app/javascript/mastodon/components/status.json"
}, },
{
"descriptors": [
{
"defaultMessage": "{resource} from other servers are not displayed.",
"id": "timeline_hint.remote_resource_not_displayed"
},
{
"defaultMessage": "Browse more on the original profile",
"id": "account.browse_more_on_origin_server"
}
],
"path": "app/javascript/mastodon/components/timeline_hint.json"
},
{ {
"descriptors": [ "descriptors": [
{ {
@ -619,6 +682,10 @@
}, },
{ {
"descriptors": [ "descriptors": [
{
"defaultMessage": "Older toots",
"id": "timeline_hint.resources.statuses"
},
{ {
"defaultMessage": "Profile unavailable", "defaultMessage": "Profile unavailable",
"id": "empty_column.account_unavailable" "id": "empty_column.account_unavailable"
@ -630,6 +697,23 @@
], ],
"path": "app/javascript/mastodon/features/account_timeline/index.json" "path": "app/javascript/mastodon/features/account_timeline/index.json"
}, },
{
"descriptors": [
{
"defaultMessage": "Click to add a note",
"id": "account_note.placeholder"
},
{
"defaultMessage": "Saved",
"id": "generic.saved"
},
{
"defaultMessage": "Note",
"id": "account.account_note_header"
}
],
"path": "app/javascript/mastodon/features/account/components/account_note.json"
},
{ {
"descriptors": [ "descriptors": [
{ {
@ -783,18 +867,6 @@
{ {
"defaultMessage": "Group", "defaultMessage": "Group",
"id": "account.badges.group" "id": "account.badges.group"
},
{
"defaultMessage": "Toots",
"id": "account.posts"
},
{
"defaultMessage": "Follows",
"id": "account.follows"
},
{
"defaultMessage": "Followers",
"id": "account.followers"
} }
], ],
"path": "app/javascript/mastodon/features/account/components/header.json" "path": "app/javascript/mastodon/features/account/components/header.json"
@ -1189,7 +1261,7 @@
{ {
"descriptors": [ "descriptors": [
{ {
"defaultMessage": "Add media ({formats})", "defaultMessage": "Add images, a video or an audio file",
"id": "upload_button.label" "id": "upload_button.label"
} }
], ],
@ -1542,6 +1614,10 @@
}, },
{ {
"descriptors": [ "descriptors": [
{
"defaultMessage": "Followers",
"id": "timeline_hint.resources.followers"
},
{ {
"defaultMessage": "Profile unavailable", "defaultMessage": "Profile unavailable",
"id": "empty_column.account_unavailable" "id": "empty_column.account_unavailable"
@ -1555,6 +1631,10 @@
}, },
{ {
"descriptors": [ "descriptors": [
{
"defaultMessage": "Follows",
"id": "timeline_hint.resources.follows"
},
{ {
"defaultMessage": "Profile unavailable", "defaultMessage": "Profile unavailable",
"id": "empty_column.account_unavailable" "id": "empty_column.account_unavailable"
@ -1920,6 +2000,10 @@
"defaultMessage": "to start a brand new toot", "defaultMessage": "to start a brand new toot",
"id": "keyboard_shortcuts.toot" "id": "keyboard_shortcuts.toot"
}, },
{
"defaultMessage": "to show/hide CW field",
"id": "keyboard_shortcuts.spoilers"
},
{ {
"defaultMessage": "to navigate back", "defaultMessage": "to navigate back",
"id": "keyboard_shortcuts.back" "id": "keyboard_shortcuts.back"
@ -2427,6 +2511,36 @@
], ],
"path": "app/javascript/mastodon/features/status/components/action_bar.json" "path": "app/javascript/mastodon/features/status/components/action_bar.json"
}, },
{
"descriptors": [
{
"defaultMessage": "Sensitive content",
"id": "status.sensitive_warning"
}
],
"path": "app/javascript/mastodon/features/status/components/card.json"
},
{
"descriptors": [
{
"defaultMessage": "Public",
"id": "privacy.public.short"
},
{
"defaultMessage": "Unlisted",
"id": "privacy.unlisted.short"
},
{
"defaultMessage": "Followers-only",
"id": "privacy.private.short"
},
{
"defaultMessage": "Direct",
"id": "privacy.direct.short"
}
],
"path": "app/javascript/mastodon/features/status/components/detailed_status.json"
},
{ {
"descriptors": [ "descriptors": [
{ {
@ -2954,10 +3068,6 @@
"defaultMessage": "Exit full screen", "defaultMessage": "Exit full screen",
"id": "video.exit_fullscreen" "id": "video.exit_fullscreen"
}, },
{
"defaultMessage": "Download file",
"id": "video.download"
},
{ {
"defaultMessage": "Sensitive content", "defaultMessage": "Sensitive content",
"id": "status.sensitive_warning" "id": "status.sensitive_warning"

View File

@ -1,10 +1,13 @@
{ {
"account.account_note_header": "Your note for @{name}",
"account.add_account_note": "Add note for @{name}",
"account.add_or_remove_from_list": "Προσθήκη ή Αφαίρεση από λίστες", "account.add_or_remove_from_list": "Προσθήκη ή Αφαίρεση από λίστες",
"account.badges.bot": "Μποτ", "account.badges.bot": "Μποτ",
"account.badges.group": "Ομάδα", "account.badges.group": "Ομάδα",
"account.block": "Αποκλεισμός @{name}", "account.block": "Αποκλεισμός @{name}",
"account.block_domain": "Απόκρυψη όλων από {domain}", "account.block_domain": "Απόκρυψη όλων από {domain}",
"account.blocked": "Αποκλεισμένος/η", "account.blocked": "Αποκλεισμένος/η",
"account.browse_more_on_origin_server": "Δες περισσότερα στο αρχικό προφίλ",
"account.cancel_follow_request": "Ακύρωση αιτήματος παρακολούθησης", "account.cancel_follow_request": "Ακύρωση αιτήματος παρακολούθησης",
"account.direct": "Προσωπικό μήνυμα προς @{name}", "account.direct": "Προσωπικό μήνυμα προς @{name}",
"account.domain_blocked": "Κρυμμένος τομέας", "account.domain_blocked": "Κρυμμένος τομέας",
@ -39,6 +42,10 @@
"account.unfollow": "Διακοπή παρακολούθησης", "account.unfollow": "Διακοπή παρακολούθησης",
"account.unmute": "Διακοπή αποσιώπησης @{name}", "account.unmute": "Διακοπή αποσιώπησης @{name}",
"account.unmute_notifications": "Διακοπή αποσιώπησης ειδοποιήσεων του/της @{name}", "account.unmute_notifications": "Διακοπή αποσιώπησης ειδοποιήσεων του/της @{name}",
"account_note.cancel": "Cancel",
"account_note.edit": "Edit",
"account_note.placeholder": "No comment provided",
"account_note.save": "Save",
"alert.rate_limited.message": "Παρακαλούμε δοκίμασε ξανά αφού περάσει η {retry_time, time, medium}.", "alert.rate_limited.message": "Παρακαλούμε δοκίμασε ξανά αφού περάσει η {retry_time, time, medium}.",
"alert.rate_limited.title": "Περιορισμός συχνότητας", "alert.rate_limited.title": "Περιορισμός συχνότητας",
"alert.unexpected.message": "Προέκυψε απροσδόκητο σφάλμα.", "alert.unexpected.message": "Προέκυψε απροσδόκητο σφάλμα.",
@ -74,9 +81,9 @@
"column_header.show_settings": "Εμφάνιση ρυθμίσεων", "column_header.show_settings": "Εμφάνιση ρυθμίσεων",
"column_header.unpin": "Ξεκαρφίτσωμα", "column_header.unpin": "Ξεκαρφίτσωμα",
"column_subheading.settings": "Ρυθμίσεις", "column_subheading.settings": "Ρυθμίσεις",
"community.column_settings.local_only": "Local only", "community.column_settings.local_only": "Τοπικά μόνο",
"community.column_settings.media_only": "Μόνο πολυμέσα", "community.column_settings.media_only": "Μόνο πολυμέσα",
"community.column_settings.remote_only": "Remote only", "community.column_settings.remote_only": "Απομακρυσμένα μόνο",
"compose_form.direct_message_warning": "Αυτό το τουτ θα σταλεί μόνο στους αναφερόμενους χρήστες.", "compose_form.direct_message_warning": "Αυτό το τουτ θα σταλεί μόνο στους αναφερόμενους χρήστες.",
"compose_form.direct_message_warning_learn_more": "Μάθετε περισσότερα", "compose_form.direct_message_warning_learn_more": "Μάθετε περισσότερα",
"compose_form.hashtag_warning": "Αυτό το τουτ δεν θα εμφανίζεται κάτω από κανένα hashtag καθώς είναι αφανές. Μόνο τα δημόσια τουτ μπορούν να αναζητηθούν ανά hashtag.", "compose_form.hashtag_warning": "Αυτό το τουτ δεν θα εμφανίζεται κάτω από κανένα hashtag καθώς είναι αφανές. Μόνο τα δημόσια τουτ μπορούν να αναζητηθούν ανά hashtag.",
@ -236,6 +243,7 @@
"keyboard_shortcuts.reply": "απάντηση", "keyboard_shortcuts.reply": "απάντηση",
"keyboard_shortcuts.requests": "άνοιγμα λίστας αιτημάτων παρακολούθησης", "keyboard_shortcuts.requests": "άνοιγμα λίστας αιτημάτων παρακολούθησης",
"keyboard_shortcuts.search": "εστίαση αναζήτησης", "keyboard_shortcuts.search": "εστίαση αναζήτησης",
"keyboard_shortcuts.spoilers": "εμφάνιση/απόκρυψη πεδίου CW",
"keyboard_shortcuts.start": "άνοιγμα κολώνας \"Ξεκινώντας\"", "keyboard_shortcuts.start": "άνοιγμα κολώνας \"Ξεκινώντας\"",
"keyboard_shortcuts.toggle_hidden": "εμφάνιση/απόκρυψη κειμένου πίσω από την προειδοποίηση", "keyboard_shortcuts.toggle_hidden": "εμφάνιση/απόκρυψη κειμένου πίσω από την προειδοποίηση",
"keyboard_shortcuts.toggle_sensitivity": "εμφάνιση/απόκρυψη πολυμέσων", "keyboard_shortcuts.toggle_sensitivity": "εμφάνιση/απόκρυψη πολυμέσων",
@ -412,6 +420,10 @@
"time_remaining.minutes": "απομένουν {number, plural, one {# λεπτό} other {# λεπτά}}", "time_remaining.minutes": "απομένουν {number, plural, one {# λεπτό} other {# λεπτά}}",
"time_remaining.moments": "Απομένουν στιγμές", "time_remaining.moments": "Απομένουν στιγμές",
"time_remaining.seconds": "απομένουν {number, plural, one {# δευτερόλεπτο} other {# δευτερόλεπτα}}", "time_remaining.seconds": "απομένουν {number, plural, one {# δευτερόλεπτο} other {# δευτερόλεπτα}}",
"timeline_hint.remote_resource_not_displayed": "{resource} από άλλους διακομιστές δεν εμφανίζονται.",
"timeline_hint.resources.followers": "Ακόλουθοι",
"timeline_hint.resources.follows": "Ακολουθεί",
"timeline_hint.resources.statuses": "Παλαιότερα τουτ",
"trends.count_by_accounts": "{count} {rawCount, plural, one {άτομο μιλάει} other {άτομα μιλάνε}}", "trends.count_by_accounts": "{count} {rawCount, plural, one {άτομο μιλάει} other {άτομα μιλάνε}}",
"trends.trending_now": "Δημοφιλή τώρα", "trends.trending_now": "Δημοφιλή τώρα",
"ui.beforeunload": "Το προσχέδιό σου θα χαθεί αν φύγεις από το Mastodon.", "ui.beforeunload": "Το προσχέδιό σου θα χαθεί αν φύγεις από το Mastodon.",

View File

@ -1,10 +1,12 @@
{ {
"account.account_note_header": "Note",
"account.add_or_remove_from_list": "Add or Remove from lists", "account.add_or_remove_from_list": "Add or Remove from lists",
"account.badges.bot": "Bot", "account.badges.bot": "Bot",
"account.badges.group": "Group", "account.badges.group": "Group",
"account.block": "Block @{name}", "account.block": "Block @{name}",
"account.block_domain": "Block domain {domain}", "account.block_domain": "Block domain {domain}",
"account.blocked": "Blocked", "account.blocked": "Blocked",
"account.browse_more_on_origin_server": "Browse more on the original profile",
"account.cancel_follow_request": "Cancel follow request", "account.cancel_follow_request": "Cancel follow request",
"account.direct": "Direct message @{name}", "account.direct": "Direct message @{name}",
"account.domain_blocked": "Domain blocked", "account.domain_blocked": "Domain blocked",
@ -13,7 +15,8 @@
"account.follow": "Follow", "account.follow": "Follow",
"account.followers": "Followers", "account.followers": "Followers",
"account.followers.empty": "No one follows this user yet.", "account.followers.empty": "No one follows this user yet.",
"account.follows": "Follows", "account.followers_counter": "{count, plural, one {{counter} Follower} other {{counter} Followers}}",
"account.following_counter": "{count, plural, other {{counter} Following}}",
"account.follows.empty": "This user doesn't follow anyone yet.", "account.follows.empty": "This user doesn't follow anyone yet.",
"account.follows_you": "Follows you", "account.follows_you": "Follows you",
"account.hide_reblogs": "Hide boosts from @{name}", "account.hide_reblogs": "Hide boosts from @{name}",
@ -33,12 +36,14 @@
"account.requested": "Awaiting approval. Click to cancel follow request", "account.requested": "Awaiting approval. Click to cancel follow request",
"account.share": "Share @{name}'s profile", "account.share": "Share @{name}'s profile",
"account.show_reblogs": "Show boosts from @{name}", "account.show_reblogs": "Show boosts from @{name}",
"account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}",
"account.unblock": "Unblock @{name}", "account.unblock": "Unblock @{name}",
"account.unblock_domain": "Unblock domain {domain}", "account.unblock_domain": "Unblock domain {domain}",
"account.unendorse": "Don't feature on profile", "account.unendorse": "Don't feature on profile",
"account.unfollow": "Unfollow", "account.unfollow": "Unfollow",
"account.unmute": "Unmute @{name}", "account.unmute": "Unmute @{name}",
"account.unmute_notifications": "Unmute notifications from @{name}", "account.unmute_notifications": "Unmute notifications from @{name}",
"account_note.placeholder": "Click to add note",
"alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.",
"alert.rate_limited.title": "Rate limited", "alert.rate_limited.title": "Rate limited",
"alert.unexpected.message": "An unexpected error occurred.", "alert.unexpected.message": "An unexpected error occurred.",
@ -102,7 +107,7 @@
"confirmations.block.confirm": "Block", "confirmations.block.confirm": "Block",
"confirmations.block.message": "Are you sure you want to block {name}?", "confirmations.block.message": "Are you sure you want to block {name}?",
"confirmations.delete.confirm": "Delete", "confirmations.delete.confirm": "Delete",
"confirmations.delete.message": "Are you sure you want to delete this status?", "confirmations.delete.message": "Are you sure you want to delete this toot?",
"confirmations.delete_list.confirm": "Delete", "confirmations.delete_list.confirm": "Delete",
"confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.domain_block.confirm": "Block entire domain", "confirmations.domain_block.confirm": "Block entire domain",
@ -113,7 +118,7 @@
"confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.", "confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.",
"confirmations.mute.message": "Are you sure you want to mute {name}?", "confirmations.mute.message": "Are you sure you want to mute {name}?",
"confirmations.redraft.confirm": "Delete & redraft", "confirmations.redraft.confirm": "Delete & redraft",
"confirmations.redraft.message": "Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.", "confirmations.redraft.message": "Are you sure you want to delete this toot and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.",
"confirmations.reply.confirm": "Reply", "confirmations.reply.confirm": "Reply",
"confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Unfollow",
@ -126,7 +131,7 @@
"directory.local": "From {domain} only", "directory.local": "From {domain} only",
"directory.new_arrivals": "New arrivals", "directory.new_arrivals": "New arrivals",
"directory.recently_active": "Recently active", "directory.recently_active": "Recently active",
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "Embed this toot on your website by copying the code below.",
"embed.preview": "Here is what it will look like:", "embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity", "emoji_button.activity": "Activity",
"emoji_button.custom": "Custom", "emoji_button.custom": "Custom",
@ -155,7 +160,7 @@
"empty_column.hashtag": "There is nothing in this hashtag yet.", "empty_column.hashtag": "There is nothing in this hashtag yet.",
"empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.", "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
"empty_column.home.public_timeline": "the public timeline", "empty_column.home.public_timeline": "the public timeline",
"empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.", "empty_column.list": "There is nothing in this list yet. When members of this list post new toots, they will appear here.",
"empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.", "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.",
"empty_column.mutes": "You haven't muted any users yet.", "empty_column.mutes": "You haven't muted any users yet.",
"empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.", "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.",
@ -167,6 +172,7 @@
"follow_request.authorize": "Authorize", "follow_request.authorize": "Authorize",
"follow_request.reject": "Reject", "follow_request.reject": "Reject",
"follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.", "follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.",
"generic.saved": "Saved",
"getting_started.developers": "Developers", "getting_started.developers": "Developers",
"getting_started.directory": "Profile directory", "getting_started.directory": "Profile directory",
"getting_started.documentation": "Documentation", "getting_started.documentation": "Documentation",
@ -212,12 +218,12 @@
"keyboard_shortcuts.back": "to navigate back", "keyboard_shortcuts.back": "to navigate back",
"keyboard_shortcuts.blocked": "to open blocked users list", "keyboard_shortcuts.blocked": "to open blocked users list",
"keyboard_shortcuts.boost": "to boost", "keyboard_shortcuts.boost": "to boost",
"keyboard_shortcuts.column": "to focus a status in one of the columns", "keyboard_shortcuts.column": "to focus a toot in one of the columns",
"keyboard_shortcuts.compose": "to focus the compose textarea", "keyboard_shortcuts.compose": "to focus the compose textarea",
"keyboard_shortcuts.description": "Description", "keyboard_shortcuts.description": "Description",
"keyboard_shortcuts.direct": "to open direct messages column", "keyboard_shortcuts.direct": "to open direct messages column",
"keyboard_shortcuts.down": "to move down in the list", "keyboard_shortcuts.down": "to move down in the list",
"keyboard_shortcuts.enter": "to open status", "keyboard_shortcuts.enter": "to open toot",
"keyboard_shortcuts.favourite": "to favourite", "keyboard_shortcuts.favourite": "to favourite",
"keyboard_shortcuts.favourites": "to open favourites list", "keyboard_shortcuts.favourites": "to open favourites list",
"keyboard_shortcuts.federated": "to open federated timeline", "keyboard_shortcuts.federated": "to open federated timeline",
@ -236,6 +242,7 @@
"keyboard_shortcuts.reply": "to reply", "keyboard_shortcuts.reply": "to reply",
"keyboard_shortcuts.requests": "to open follow requests list", "keyboard_shortcuts.requests": "to open follow requests list",
"keyboard_shortcuts.search": "to focus search", "keyboard_shortcuts.search": "to focus search",
"keyboard_shortcuts.spoilers": "to show/hide CW field",
"keyboard_shortcuts.start": "to open \"get started\" column", "keyboard_shortcuts.start": "to open \"get started\" column",
"keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toggle_sensitivity": "to show/hide media", "keyboard_shortcuts.toggle_sensitivity": "to show/hide media",
@ -284,13 +291,13 @@
"navigation_bar.preferences": "Preferences", "navigation_bar.preferences": "Preferences",
"navigation_bar.public_timeline": "Federated timeline", "navigation_bar.public_timeline": "Federated timeline",
"navigation_bar.security": "Security", "navigation_bar.security": "Security",
"notification.favourite": "{name} favourited your status", "notification.favourite": "{name} favourited your toot",
"notification.follow": "{name} followed you", "notification.follow": "{name} followed you",
"notification.follow_request": "{name} has requested to follow you", "notification.follow_request": "{name} has requested to follow you",
"notification.mention": "{name} mentioned you", "notification.mention": "{name} mentioned you",
"notification.own_poll": "Your poll has ended", "notification.own_poll": "Your poll has ended",
"notification.poll": "A poll you have voted in has ended", "notification.poll": "A poll you have voted in has ended",
"notification.reblog": "{name} boosted your status", "notification.reblog": "{name} boosted your toot",
"notifications.clear": "Clear notifications", "notifications.clear": "Clear notifications",
"notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
"notifications.column_settings.alert": "Desktop notifications", "notifications.column_settings.alert": "Desktop notifications",
@ -321,7 +328,7 @@
"poll.voted": "You voted for this answer", "poll.voted": "You voted for this answer",
"poll_button.add_poll": "Add a poll", "poll_button.add_poll": "Add a poll",
"poll_button.remove_poll": "Remove poll", "poll_button.remove_poll": "Remove poll",
"privacy.change": "Adjust status privacy", "privacy.change": "Adjust toot privacy",
"privacy.direct.long": "Visible for mentioned users only", "privacy.direct.long": "Visible for mentioned users only",
"privacy.direct.short": "Direct", "privacy.direct.short": "Direct",
"privacy.private.long": "Visible for followers only", "privacy.private.long": "Visible for followers only",
@ -348,9 +355,9 @@
"report.target": "Reporting {target}", "report.target": "Reporting {target}",
"search.placeholder": "Search", "search.placeholder": "Search",
"search_popout.search_format": "Advanced search format", "search_popout.search_format": "Advanced search format",
"search_popout.tips.full_text": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", "search_popout.tips.full_text": "Simple text returns toots you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.",
"search_popout.tips.hashtag": "hashtag", "search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "status", "search_popout.tips.status": "toot",
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
"search_popout.tips.user": "user", "search_popout.tips.user": "user",
"search_results.accounts": "People", "search_results.accounts": "People",
@ -359,12 +366,12 @@
"search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.", "search_results.statuses_fts_disabled": "Searching toots by their content is not enabled on this Mastodon server.",
"search_results.total": "{count, number} {count, plural, one {result} other {results}}", "search_results.total": "{count, number} {count, plural, one {result} other {results}}",
"status.admin_account": "Open moderation interface for @{name}", "status.admin_account": "Open moderation interface for @{name}",
"status.admin_status": "Open this status in the moderation interface", "status.admin_status": "Open this toot in the moderation interface",
"status.block": "Block @{name}", "status.block": "Block @{name}",
"status.bookmark": "Bookmark", "status.bookmark": "Bookmark",
"status.cancel_reblog_private": "Unboost", "status.cancel_reblog_private": "Unboost",
"status.cannot_reblog": "This post cannot be boosted", "status.cannot_reblog": "This post cannot be boosted",
"status.copy": "Copy link to status", "status.copy": "Copy link to toot",
"status.delete": "Delete", "status.delete": "Delete",
"status.detailed_status": "Detailed conversation view", "status.detailed_status": "Detailed conversation view",
"status.direct": "Direct message @{name}", "status.direct": "Direct message @{name}",
@ -377,7 +384,7 @@
"status.more": "More", "status.more": "More",
"status.mute": "Mute @{name}", "status.mute": "Mute @{name}",
"status.mute_conversation": "Mute conversation", "status.mute_conversation": "Mute conversation",
"status.open": "Expand this status", "status.open": "Expand this toot",
"status.pin": "Pin on profile", "status.pin": "Pin on profile",
"status.pinned": "Pinned toot", "status.pinned": "Pinned toot",
"status.read_more": "Read more", "status.read_more": "Read more",
@ -412,11 +419,18 @@
"time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
"time_remaining.moments": "Moments remaining", "time_remaining.moments": "Moments remaining",
"time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
"trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "timeline_hint.remote_resource_not_displayed": "{resource} from other servers are not displayed.",
"timeline_hint.resources.followers": "Followers",
"timeline_hint.resources.follows": "Follows",
"timeline_hint.resources.statuses": "Older toots",
"trends.counter_by_accounts": "{count, plural, one {{counter} person} other {{counter} people}} talking",
"trends.trending_now": "Trending now", "trends.trending_now": "Trending now",
"ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
"units.short.billion": "{count}B",
"units.short.million": "{count}M",
"units.short.thousand": "{count}K",
"upload_area.title": "Drag & drop to upload", "upload_area.title": "Drag & drop to upload",
"upload_button.label": "Add media ({formats})", "upload_button.label": "Add images, a video or an audio file",
"upload_error.limit": "File upload limit exceeded.", "upload_error.limit": "File upload limit exceeded.",
"upload_error.poll": "File upload not allowed with polls.", "upload_error.poll": "File upload not allowed with polls.",
"upload_form.audio_description": "Describe for people with hearing loss", "upload_form.audio_description": "Describe for people with hearing loss",

View File

@ -1,10 +1,13 @@
{ {
"account.account_note_header": "Your note for @{name}",
"account.add_account_note": "Add note for @{name}",
"account.add_or_remove_from_list": "Aldoni al aŭ forigi el listoj", "account.add_or_remove_from_list": "Aldoni al aŭ forigi el listoj",
"account.badges.bot": "Roboto", "account.badges.bot": "Roboto",
"account.badges.group": "Grupo", "account.badges.group": "Grupo",
"account.block": "Bloki @{name}", "account.block": "Bloki @{name}",
"account.block_domain": "Kaŝi ĉion de {domain}", "account.block_domain": "Kaŝi ĉion de {domain}",
"account.blocked": "Blokita", "account.blocked": "Blokita",
"account.browse_more_on_origin_server": "Browse more on the original profile",
"account.cancel_follow_request": "Nuligi peton de sekvado", "account.cancel_follow_request": "Nuligi peton de sekvado",
"account.direct": "Rekte mesaĝi @{name}", "account.direct": "Rekte mesaĝi @{name}",
"account.domain_blocked": "Domajno kaŝita", "account.domain_blocked": "Domajno kaŝita",
@ -39,6 +42,10 @@
"account.unfollow": "Ne plu sekvi", "account.unfollow": "Ne plu sekvi",
"account.unmute": "Malsilentigi @{name}", "account.unmute": "Malsilentigi @{name}",
"account.unmute_notifications": "Malsilentigi sciigojn de @{name}", "account.unmute_notifications": "Malsilentigi sciigojn de @{name}",
"account_note.cancel": "Cancel",
"account_note.edit": "Edit",
"account_note.placeholder": "No comment provided",
"account_note.save": "Save",
"alert.rate_limited.message": "Bonvolu reprovi post {retry_time, time, medium}.", "alert.rate_limited.message": "Bonvolu reprovi post {retry_time, time, medium}.",
"alert.rate_limited.title": "Mesaĝkvante limigita", "alert.rate_limited.title": "Mesaĝkvante limigita",
"alert.unexpected.message": "Neatendita eraro okazis.", "alert.unexpected.message": "Neatendita eraro okazis.",
@ -74,9 +81,9 @@
"column_header.show_settings": "Montri agordojn", "column_header.show_settings": "Montri agordojn",
"column_header.unpin": "Depingli", "column_header.unpin": "Depingli",
"column_subheading.settings": "Agordado", "column_subheading.settings": "Agordado",
"community.column_settings.local_only": "Local only", "community.column_settings.local_only": "Nur loka",
"community.column_settings.media_only": "Nur aŭdovidaĵoj", "community.column_settings.media_only": "Nur aŭdovidaĵoj",
"community.column_settings.remote_only": "Remote only", "community.column_settings.remote_only": "Nur malproksima",
"compose_form.direct_message_warning": "Tiu mesaĝo estos sendita nur al menciitaj uzantoj.", "compose_form.direct_message_warning": "Tiu mesaĝo estos sendita nur al menciitaj uzantoj.",
"compose_form.direct_message_warning_learn_more": "Lerni pli", "compose_form.direct_message_warning_learn_more": "Lerni pli",
"compose_form.hashtag_warning": "Ĉi tiu mesaĝo ne estos listigita per ajna kradvorto. Nur publikaj mesaĝoj estas serĉeblaj per kradvortoj.", "compose_form.hashtag_warning": "Ĉi tiu mesaĝo ne estos listigita per ajna kradvorto. Nur publikaj mesaĝoj estas serĉeblaj per kradvortoj.",
@ -236,6 +243,7 @@
"keyboard_shortcuts.reply": "respondi", "keyboard_shortcuts.reply": "respondi",
"keyboard_shortcuts.requests": "malfermi la liston de petoj de sekvado", "keyboard_shortcuts.requests": "malfermi la liston de petoj de sekvado",
"keyboard_shortcuts.search": "enfokusigi la serĉilon", "keyboard_shortcuts.search": "enfokusigi la serĉilon",
"keyboard_shortcuts.spoilers": "to show/hide CW field",
"keyboard_shortcuts.start": "malfermi la kolumnon «por komenci»", "keyboard_shortcuts.start": "malfermi la kolumnon «por komenci»",
"keyboard_shortcuts.toggle_hidden": "montri/kaŝi tekston malantaŭ enhava averto", "keyboard_shortcuts.toggle_hidden": "montri/kaŝi tekston malantaŭ enhava averto",
"keyboard_shortcuts.toggle_sensitivity": "montri/kaŝi aŭdovidaĵojn", "keyboard_shortcuts.toggle_sensitivity": "montri/kaŝi aŭdovidaĵojn",
@ -412,6 +420,10 @@
"time_remaining.minutes": "{number, plural, one {# minuto} other {# minutoj}} restas", "time_remaining.minutes": "{number, plural, one {# minuto} other {# minutoj}} restas",
"time_remaining.moments": "Momenteto restas", "time_remaining.moments": "Momenteto restas",
"time_remaining.seconds": "{number, plural, one {# sekundo} other {# sekundoj}} restas", "time_remaining.seconds": "{number, plural, one {# sekundo} other {# sekundoj}} restas",
"timeline_hint.remote_resource_not_displayed": "{resource} from other servers are not displayed.",
"timeline_hint.resources.followers": "Followers",
"timeline_hint.resources.follows": "Follows",
"timeline_hint.resources.statuses": "Older toots",
"trends.count_by_accounts": "{count} {rawCount, plural, one {persono} other {personoj}} parolas", "trends.count_by_accounts": "{count} {rawCount, plural, one {persono} other {personoj}} parolas",
"trends.trending_now": "Nunaj furoraĵoj", "trends.trending_now": "Nunaj furoraĵoj",
"ui.beforeunload": "Via malneto perdiĝos se vi eliras de Mastodon.", "ui.beforeunload": "Via malneto perdiĝos se vi eliras de Mastodon.",

View File

@ -1,10 +1,13 @@
{ {
"account.account_note_header": "Your note for @{name}",
"account.add_account_note": "Add note for @{name}",
"account.add_or_remove_from_list": "Agregar o quitar de las listas", "account.add_or_remove_from_list": "Agregar o quitar de las listas",
"account.badges.bot": "Bot", "account.badges.bot": "Bot",
"account.badges.group": "Grupo", "account.badges.group": "Grupo",
"account.block": "Bloquear a @{name}", "account.block": "Bloquear a @{name}",
"account.block_domain": "Ocultar todo de {domain}", "account.block_domain": "Ocultar todo de {domain}",
"account.blocked": "Bloqueado", "account.blocked": "Bloqueado",
"account.browse_more_on_origin_server": "Explorar más en el perfil original",
"account.cancel_follow_request": "Cancelar la solicitud de seguimiento", "account.cancel_follow_request": "Cancelar la solicitud de seguimiento",
"account.direct": "Mensaje directo a @{name}", "account.direct": "Mensaje directo a @{name}",
"account.domain_blocked": "Dominio oculto", "account.domain_blocked": "Dominio oculto",
@ -39,6 +42,10 @@
"account.unfollow": "Dejar de seguir", "account.unfollow": "Dejar de seguir",
"account.unmute": "Dejar de silenciar a @{name}", "account.unmute": "Dejar de silenciar a @{name}",
"account.unmute_notifications": "Dejar de silenciar las notificaciones de @{name}", "account.unmute_notifications": "Dejar de silenciar las notificaciones de @{name}",
"account_note.cancel": "Cancel",
"account_note.edit": "Edit",
"account_note.placeholder": "No comment provided",
"account_note.save": "Save",
"alert.rate_limited.message": "Por favor, reintentá después de las {retry_time, time, medium}.", "alert.rate_limited.message": "Por favor, reintentá después de las {retry_time, time, medium}.",
"alert.rate_limited.title": "Tarifa limitada", "alert.rate_limited.title": "Tarifa limitada",
"alert.unexpected.message": "Ocurrió un error.", "alert.unexpected.message": "Ocurrió un error.",
@ -74,9 +81,9 @@
"column_header.show_settings": "Mostrar configuración", "column_header.show_settings": "Mostrar configuración",
"column_header.unpin": "Dejar de fijar", "column_header.unpin": "Dejar de fijar",
"column_subheading.settings": "Configuración", "column_subheading.settings": "Configuración",
"community.column_settings.local_only": "Local only", "community.column_settings.local_only": "Sólo local",
"community.column_settings.media_only": "Sólo medios", "community.column_settings.media_only": "Sólo medios",
"community.column_settings.remote_only": "Remote only", "community.column_settings.remote_only": "Sólo remoto",
"compose_form.direct_message_warning": "Este toot sólo será enviado a los usuarios mencionados.", "compose_form.direct_message_warning": "Este toot sólo será enviado a los usuarios mencionados.",
"compose_form.direct_message_warning_learn_more": "Aprendé más", "compose_form.direct_message_warning_learn_more": "Aprendé más",
"compose_form.hashtag_warning": "Este toot no se mostrará bajo hashtags porque no es público. Sólo los toots públicos se pueden buscar por hashtag.", "compose_form.hashtag_warning": "Este toot no se mostrará bajo hashtags porque no es público. Sólo los toots públicos se pueden buscar por hashtag.",
@ -236,6 +243,7 @@
"keyboard_shortcuts.reply": "para responder", "keyboard_shortcuts.reply": "para responder",
"keyboard_shortcuts.requests": "para abrir la lista de solicitudes de seguimiento", "keyboard_shortcuts.requests": "para abrir la lista de solicitudes de seguimiento",
"keyboard_shortcuts.search": "para enfocar la búsqueda", "keyboard_shortcuts.search": "para enfocar la búsqueda",
"keyboard_shortcuts.spoilers": "para mostrar/ocultar el campo \"CW\"",
"keyboard_shortcuts.start": "para abrir la columna \"Introducción\"", "keyboard_shortcuts.start": "para abrir la columna \"Introducción\"",
"keyboard_shortcuts.toggle_hidden": "para mostrar/ocultar el texto detrás de la advertencia de contenido", "keyboard_shortcuts.toggle_hidden": "para mostrar/ocultar el texto detrás de la advertencia de contenido",
"keyboard_shortcuts.toggle_sensitivity": "para mostrar/ocultar los medios", "keyboard_shortcuts.toggle_sensitivity": "para mostrar/ocultar los medios",
@ -412,6 +420,10 @@
"time_remaining.minutes": "{number, plural,one {queda # minuto} other {quedan # minutos}}", "time_remaining.minutes": "{number, plural,one {queda # minuto} other {quedan # minutos}}",
"time_remaining.moments": "Momentos restantes", "time_remaining.moments": "Momentos restantes",
"time_remaining.seconds": "{number, plural,one {queda # segundo} other {quedan # segundos}}", "time_remaining.seconds": "{number, plural,one {queda # segundo} other {quedan # segundos}}",
"timeline_hint.remote_resource_not_displayed": "{resource} de otros servidores no se muestran.",
"timeline_hint.resources.followers": "Seguidores",
"timeline_hint.resources.follows": "Siguiendo",
"timeline_hint.resources.statuses": "Toots antiguos",
"trends.count_by_accounts": "{count} {rawCount, plural, one {persona} other {personas}} hablando", "trends.count_by_accounts": "{count} {rawCount, plural, one {persona} other {personas}} hablando",
"trends.trending_now": "Tendencia ahora", "trends.trending_now": "Tendencia ahora",
"ui.beforeunload": "Tu borrador se perderá si abandonás Mastodon.", "ui.beforeunload": "Tu borrador se perderá si abandonás Mastodon.",

View File

@ -1,10 +1,13 @@
{ {
"account.account_note_header": "Your note for @{name}",
"account.add_account_note": "Add note for @{name}",
"account.add_or_remove_from_list": "Agregar o eliminar de listas", "account.add_or_remove_from_list": "Agregar o eliminar de listas",
"account.badges.bot": "Bot", "account.badges.bot": "Bot",
"account.badges.group": "Grupo", "account.badges.group": "Grupo",
"account.block": "Bloquear a @{name}", "account.block": "Bloquear a @{name}",
"account.block_domain": "Ocultar todo de {domain}", "account.block_domain": "Ocultar todo de {domain}",
"account.blocked": "Bloqueado", "account.blocked": "Bloqueado",
"account.browse_more_on_origin_server": "Ver más en el perfil original",
"account.cancel_follow_request": "Cancelar la solicitud de seguimiento", "account.cancel_follow_request": "Cancelar la solicitud de seguimiento",
"account.direct": "Mensaje directo a @{name}", "account.direct": "Mensaje directo a @{name}",
"account.domain_blocked": "Dominio oculto", "account.domain_blocked": "Dominio oculto",
@ -39,6 +42,10 @@
"account.unfollow": "Dejar de seguir", "account.unfollow": "Dejar de seguir",
"account.unmute": "Dejar de silenciar a @{name}", "account.unmute": "Dejar de silenciar a @{name}",
"account.unmute_notifications": "Dejar de silenciar las notificaciones de @{name}", "account.unmute_notifications": "Dejar de silenciar las notificaciones de @{name}",
"account_note.cancel": "Cancel",
"account_note.edit": "Edit",
"account_note.placeholder": "No comment provided",
"account_note.save": "Save",
"alert.rate_limited.message": "Por favor reintente después de {retry_time, time, medium}.", "alert.rate_limited.message": "Por favor reintente después de {retry_time, time, medium}.",
"alert.rate_limited.title": "Tarifa limitada", "alert.rate_limited.title": "Tarifa limitada",
"alert.unexpected.message": "Hubo un error inesperado.", "alert.unexpected.message": "Hubo un error inesperado.",
@ -74,9 +81,9 @@
"column_header.show_settings": "Mostrar ajustes", "column_header.show_settings": "Mostrar ajustes",
"column_header.unpin": "Dejar de fijar", "column_header.unpin": "Dejar de fijar",
"column_subheading.settings": "Ajustes", "column_subheading.settings": "Ajustes",
"community.column_settings.local_only": "Local only", "community.column_settings.local_only": "Solo local",
"community.column_settings.media_only": "Solo media", "community.column_settings.media_only": "Solo media",
"community.column_settings.remote_only": "Remote only", "community.column_settings.remote_only": "Solo remoto",
"compose_form.direct_message_warning": "Este toot solo será enviado a los usuarios mencionados.", "compose_form.direct_message_warning": "Este toot solo será enviado a los usuarios mencionados.",
"compose_form.direct_message_warning_learn_more": "Aprender mas", "compose_form.direct_message_warning_learn_more": "Aprender mas",
"compose_form.hashtag_warning": "Este toot no se mostrará bajo hashtags porque no es público. Sólo los toots públicos se pueden buscar por hashtag.", "compose_form.hashtag_warning": "Este toot no se mostrará bajo hashtags porque no es público. Sólo los toots públicos se pueden buscar por hashtag.",
@ -236,6 +243,7 @@
"keyboard_shortcuts.reply": "para responder", "keyboard_shortcuts.reply": "para responder",
"keyboard_shortcuts.requests": "abrir la lista de peticiones de seguidores", "keyboard_shortcuts.requests": "abrir la lista de peticiones de seguidores",
"keyboard_shortcuts.search": "para poner el foco en la búsqueda", "keyboard_shortcuts.search": "para poner el foco en la búsqueda",
"keyboard_shortcuts.spoilers": "para mostrar/ocultar el campo CW",
"keyboard_shortcuts.start": "abrir la columna \"comenzar\"", "keyboard_shortcuts.start": "abrir la columna \"comenzar\"",
"keyboard_shortcuts.toggle_hidden": "mostrar/ocultar texto tras aviso de contenido (CW)", "keyboard_shortcuts.toggle_hidden": "mostrar/ocultar texto tras aviso de contenido (CW)",
"keyboard_shortcuts.toggle_sensitivity": "mostrar/ocultar medios", "keyboard_shortcuts.toggle_sensitivity": "mostrar/ocultar medios",
@ -412,6 +420,10 @@
"time_remaining.minutes": "{number, plural, one {# minuto restante} other {# minutos restantes}}", "time_remaining.minutes": "{number, plural, one {# minuto restante} other {# minutos restantes}}",
"time_remaining.moments": "Momentos restantes", "time_remaining.moments": "Momentos restantes",
"time_remaining.seconds": "{number, plural, one {# segundo restante} other {# segundos restantes}}", "time_remaining.seconds": "{number, plural, one {# segundo restante} other {# segundos restantes}}",
"timeline_hint.remote_resource_not_displayed": "{resource} de otros servidores no se muestran.",
"timeline_hint.resources.followers": "Seguidores",
"timeline_hint.resources.follows": "Seguidos",
"timeline_hint.resources.statuses": "Toots más antiguos",
"trends.count_by_accounts": "{count} {rawCount, plural, one {persona} other {personas}} hablando", "trends.count_by_accounts": "{count} {rawCount, plural, one {persona} other {personas}} hablando",
"trends.trending_now": "Tendencia ahora", "trends.trending_now": "Tendencia ahora",
"ui.beforeunload": "Tu borrador se perderá si sales de Mastodon.", "ui.beforeunload": "Tu borrador se perderá si sales de Mastodon.",

View File

@ -1,10 +1,13 @@
{ {
"account.account_note_header": "Your note for @{name}",
"account.add_account_note": "Add note for @{name}",
"account.add_or_remove_from_list": "Lisa või Eemalda nimekirjadest", "account.add_or_remove_from_list": "Lisa või Eemalda nimekirjadest",
"account.badges.bot": "Robot", "account.badges.bot": "Robot",
"account.badges.group": "Grupp", "account.badges.group": "Grupp",
"account.block": "Blokeeri @{name}", "account.block": "Blokeeri @{name}",
"account.block_domain": "Peida kõik domeenist {domain}", "account.block_domain": "Peida kõik domeenist {domain}",
"account.blocked": "Blokeeritud", "account.blocked": "Blokeeritud",
"account.browse_more_on_origin_server": "Browse more on the original profile",
"account.cancel_follow_request": "Tühista jälgimistaotlus", "account.cancel_follow_request": "Tühista jälgimistaotlus",
"account.direct": "Otsesõnum @{name}", "account.direct": "Otsesõnum @{name}",
"account.domain_blocked": "Domeen peidetud", "account.domain_blocked": "Domeen peidetud",
@ -39,6 +42,10 @@
"account.unfollow": "Ära jälgi", "account.unfollow": "Ära jälgi",
"account.unmute": "Ära vaigista @{name}", "account.unmute": "Ära vaigista @{name}",
"account.unmute_notifications": "Ära vaigista teateid kasutajalt @{name}", "account.unmute_notifications": "Ära vaigista teateid kasutajalt @{name}",
"account_note.cancel": "Cancel",
"account_note.edit": "Edit",
"account_note.placeholder": "No comment provided",
"account_note.save": "Save",
"alert.rate_limited.message": "Palun proovi uuesti pärast {retry_time, time, medium}.", "alert.rate_limited.message": "Palun proovi uuesti pärast {retry_time, time, medium}.",
"alert.rate_limited.title": "Piiratud", "alert.rate_limited.title": "Piiratud",
"alert.unexpected.message": "Tekkis ootamatu viga.", "alert.unexpected.message": "Tekkis ootamatu viga.",
@ -74,9 +81,9 @@
"column_header.show_settings": "Näita sätteid", "column_header.show_settings": "Näita sätteid",
"column_header.unpin": "Eemalda kinnitus", "column_header.unpin": "Eemalda kinnitus",
"column_subheading.settings": "Sätted", "column_subheading.settings": "Sätted",
"community.column_settings.local_only": "Local only", "community.column_settings.local_only": "Ainult kohalik",
"community.column_settings.media_only": "Ainult meedia", "community.column_settings.media_only": "Ainult meedia",
"community.column_settings.remote_only": "Remote only", "community.column_settings.remote_only": "Ainult kaug",
"compose_form.direct_message_warning": "See tuut saadetakse ainult mainitud kasutajatele.", "compose_form.direct_message_warning": "See tuut saadetakse ainult mainitud kasutajatele.",
"compose_form.direct_message_warning_learn_more": "Vaata veel", "compose_form.direct_message_warning_learn_more": "Vaata veel",
"compose_form.hashtag_warning": "Seda tuuti ei kuvata ühegi sildi all, sest see on kirjendamata. Ainult avalikud tuutid on sildi järgi otsitavad.", "compose_form.hashtag_warning": "Seda tuuti ei kuvata ühegi sildi all, sest see on kirjendamata. Ainult avalikud tuutid on sildi järgi otsitavad.",
@ -166,7 +173,7 @@
"errors.unexpected_crash.report_issue": "Teavita veast", "errors.unexpected_crash.report_issue": "Teavita veast",
"follow_request.authorize": "Autoriseeri", "follow_request.authorize": "Autoriseeri",
"follow_request.reject": "Hülga", "follow_request.reject": "Hülga",
"follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.", "follow_requests.unlocked_explanation": "Kuigi Teie konto pole lukustatud, soovitab {domain} personal siiski manuaalselt üle vaadata jälgimistaotlused nendelt kontodelt.",
"getting_started.developers": "Arendajad", "getting_started.developers": "Arendajad",
"getting_started.directory": "Profiili kataloog", "getting_started.directory": "Profiili kataloog",
"getting_started.documentation": "Dokumentatsioon", "getting_started.documentation": "Dokumentatsioon",
@ -236,6 +243,7 @@
"keyboard_shortcuts.reply": "vastamiseks", "keyboard_shortcuts.reply": "vastamiseks",
"keyboard_shortcuts.requests": "avamaks jälgimistaotluste nimistut", "keyboard_shortcuts.requests": "avamaks jälgimistaotluste nimistut",
"keyboard_shortcuts.search": "otsingu fokuseerimiseks", "keyboard_shortcuts.search": "otsingu fokuseerimiseks",
"keyboard_shortcuts.spoilers": "to show/hide CW field",
"keyboard_shortcuts.start": "avamaks \"Alusta\" tulpa", "keyboard_shortcuts.start": "avamaks \"Alusta\" tulpa",
"keyboard_shortcuts.toggle_hidden": "näitamaks/peitmaks teksti CW taga", "keyboard_shortcuts.toggle_hidden": "näitamaks/peitmaks teksti CW taga",
"keyboard_shortcuts.toggle_sensitivity": "et peita/näidata meediat", "keyboard_shortcuts.toggle_sensitivity": "et peita/näidata meediat",
@ -412,6 +420,10 @@
"time_remaining.minutes": "{number, plural, one {# minut} other {# minutit}} left", "time_remaining.minutes": "{number, plural, one {# minut} other {# minutit}} left",
"time_remaining.moments": "Hetked jäänud", "time_remaining.moments": "Hetked jäänud",
"time_remaining.seconds": "{number, plural, one {# sekund} other {# sekundit}} left", "time_remaining.seconds": "{number, plural, one {# sekund} other {# sekundit}} left",
"timeline_hint.remote_resource_not_displayed": "{resource} from other servers are not displayed.",
"timeline_hint.resources.followers": "Followers",
"timeline_hint.resources.follows": "Follows",
"timeline_hint.resources.statuses": "Older toots",
"trends.count_by_accounts": "{count} {rawCount, plural, one {inimene} other {inimesed}} talking", "trends.count_by_accounts": "{count} {rawCount, plural, one {inimene} other {inimesed}} talking",
"trends.trending_now": "Praegu populaarne", "trends.trending_now": "Praegu populaarne",
"ui.beforeunload": "Teie mustand läheb kaotsi, kui lahkute Mastodonist.", "ui.beforeunload": "Teie mustand läheb kaotsi, kui lahkute Mastodonist.",

View File

@ -1,10 +1,13 @@
{ {
"account.account_note_header": "Your note for @{name}",
"account.add_account_note": "Add note for @{name}",
"account.add_or_remove_from_list": "Gehitu edo kendu zerrendetatik", "account.add_or_remove_from_list": "Gehitu edo kendu zerrendetatik",
"account.badges.bot": "Bot-a", "account.badges.bot": "Bot-a",
"account.badges.group": "Taldea", "account.badges.group": "Taldea",
"account.block": "Blokeatu @{name}", "account.block": "Blokeatu @{name}",
"account.block_domain": "Ezkutatu {domain} domeinuko guztia", "account.block_domain": "Ezkutatu {domain} domeinuko guztia",
"account.blocked": "Blokeatuta", "account.blocked": "Blokeatuta",
"account.browse_more_on_origin_server": "Browse more on the original profile",
"account.cancel_follow_request": "Ezeztatu jarraitzeko eskaria", "account.cancel_follow_request": "Ezeztatu jarraitzeko eskaria",
"account.direct": "Mezu zuzena @{name}(r)i", "account.direct": "Mezu zuzena @{name}(r)i",
"account.domain_blocked": "Ezkutatutako domeinua", "account.domain_blocked": "Ezkutatutako domeinua",
@ -39,6 +42,10 @@
"account.unfollow": "Utzi jarraitzeari", "account.unfollow": "Utzi jarraitzeari",
"account.unmute": "Desmututu @{name}", "account.unmute": "Desmututu @{name}",
"account.unmute_notifications": "Desmututu @{name}(r)en jakinarazpenak", "account.unmute_notifications": "Desmututu @{name}(r)en jakinarazpenak",
"account_note.cancel": "Cancel",
"account_note.edit": "Edit",
"account_note.placeholder": "No comment provided",
"account_note.save": "Save",
"alert.rate_limited.message": "Saiatu {retry_time, time, medium} barru.", "alert.rate_limited.message": "Saiatu {retry_time, time, medium} barru.",
"alert.rate_limited.title": "Abiadura mugatua", "alert.rate_limited.title": "Abiadura mugatua",
"alert.unexpected.message": "Ustekabeko errore bat gertatu da.", "alert.unexpected.message": "Ustekabeko errore bat gertatu da.",
@ -74,9 +81,9 @@
"column_header.show_settings": "Erakutsi ezarpenak", "column_header.show_settings": "Erakutsi ezarpenak",
"column_header.unpin": "Desfinkatu", "column_header.unpin": "Desfinkatu",
"column_subheading.settings": "Ezarpenak", "column_subheading.settings": "Ezarpenak",
"community.column_settings.local_only": "Local only", "community.column_settings.local_only": "Lokala soilik",
"community.column_settings.media_only": "Multimedia besterik ez", "community.column_settings.media_only": "Multimedia besterik ez",
"community.column_settings.remote_only": "Remote only", "community.column_settings.remote_only": "Urrunekoa soilik",
"compose_form.direct_message_warning": "Toot hau aipatutako erabiltzaileei besterik ez zaie bidaliko.", "compose_form.direct_message_warning": "Toot hau aipatutako erabiltzaileei besterik ez zaie bidaliko.",
"compose_form.direct_message_warning_learn_more": "Ikasi gehiago", "compose_form.direct_message_warning_learn_more": "Ikasi gehiago",
"compose_form.hashtag_warning": "Toot hau ez da traoletan agertuko zerrendatu gabekoa baita. Traoletan toot publikoak besterik ez dira agertzen.", "compose_form.hashtag_warning": "Toot hau ez da traoletan agertuko zerrendatu gabekoa baita. Traoletan toot publikoak besterik ez dira agertzen.",
@ -166,7 +173,7 @@
"errors.unexpected_crash.report_issue": "Eman arazoaren berri", "errors.unexpected_crash.report_issue": "Eman arazoaren berri",
"follow_request.authorize": "Baimendu", "follow_request.authorize": "Baimendu",
"follow_request.reject": "Ukatu", "follow_request.reject": "Ukatu",
"follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.", "follow_requests.unlocked_explanation": "Zure kontua blokeatuta ez badago ere, {domain} domeinuko arduradunek uste dute kontu hauetako jarraipen eskariak agian eskuz begiratu nahiko dituzula.",
"getting_started.developers": "Garatzaileak", "getting_started.developers": "Garatzaileak",
"getting_started.directory": "Profil-direktorioa", "getting_started.directory": "Profil-direktorioa",
"getting_started.documentation": "Dokumentazioa", "getting_started.documentation": "Dokumentazioa",
@ -236,6 +243,7 @@
"keyboard_shortcuts.reply": "erantzutea", "keyboard_shortcuts.reply": "erantzutea",
"keyboard_shortcuts.requests": "jarraitzeko eskarien zerrenda irekitzeko", "keyboard_shortcuts.requests": "jarraitzeko eskarien zerrenda irekitzeko",
"keyboard_shortcuts.search": "bilaketan fokua jartzea", "keyboard_shortcuts.search": "bilaketan fokua jartzea",
"keyboard_shortcuts.spoilers": "to show/hide CW field",
"keyboard_shortcuts.start": "\"Menua\" zutabea irekitzeko", "keyboard_shortcuts.start": "\"Menua\" zutabea irekitzeko",
"keyboard_shortcuts.toggle_hidden": "testua erakustea/ezkutatzea abisu baten atzean", "keyboard_shortcuts.toggle_hidden": "testua erakustea/ezkutatzea abisu baten atzean",
"keyboard_shortcuts.toggle_sensitivity": "multimedia erakutsi/ezkutatzeko", "keyboard_shortcuts.toggle_sensitivity": "multimedia erakutsi/ezkutatzeko",
@ -412,6 +420,10 @@
"time_remaining.minutes": "{number, plural, one {minutu #} other {# minutu}} amaitzeko", "time_remaining.minutes": "{number, plural, one {minutu #} other {# minutu}} amaitzeko",
"time_remaining.moments": "Amaitzekotan", "time_remaining.moments": "Amaitzekotan",
"time_remaining.seconds": "{number, plural, one {segundo #} other {# segundo}} amaitzeko", "time_remaining.seconds": "{number, plural, one {segundo #} other {# segundo}} amaitzeko",
"timeline_hint.remote_resource_not_displayed": "{resource} from other servers are not displayed.",
"timeline_hint.resources.followers": "Followers",
"timeline_hint.resources.follows": "Follows",
"timeline_hint.resources.statuses": "Older toots",
"trends.count_by_accounts": "{count} {rawCount, plural, one {pertsona} other {pertsona}} hitz egiten", "trends.count_by_accounts": "{count} {rawCount, plural, one {pertsona} other {pertsona}} hitz egiten",
"trends.trending_now": "Joera orain", "trends.trending_now": "Joera orain",
"ui.beforeunload": "Zure zirriborroa galduko da Mastodon uzten baduzu.", "ui.beforeunload": "Zure zirriborroa galduko da Mastodon uzten baduzu.",

View File

@ -1,10 +1,13 @@
{ {
"account.account_note_header": "Your note for @{name}",
"account.add_account_note": "Add note for @{name}",
"account.add_or_remove_from_list": "افزودن یا برداشتن از فهرست‌ها", "account.add_or_remove_from_list": "افزودن یا برداشتن از فهرست‌ها",
"account.badges.bot": "ربات", "account.badges.bot": "ربات",
"account.badges.group": "گروه", "account.badges.group": "گروه",
"account.block": "مسدودسازی @{name}", "account.block": "مسدودسازی @{name}",
"account.block_domain": "نهفتن همه چیز از {domain}", "account.block_domain": "نهفتن همه چیز از {domain}",
"account.blocked": "مسدود", "account.blocked": "مسدود",
"account.browse_more_on_origin_server": "مرور بیش‌تر روی نمایهٔ اصلی",
"account.cancel_follow_request": "لغو درخواست پیگیری", "account.cancel_follow_request": "لغو درخواست پیگیری",
"account.direct": "پیام خصوصی به @{name}", "account.direct": "پیام خصوصی به @{name}",
"account.domain_blocked": "دامنهٔ نهفته", "account.domain_blocked": "دامنهٔ نهفته",
@ -27,7 +30,7 @@
"account.mute_notifications": "خموشاندن اعلان‌ها از @{name}", "account.mute_notifications": "خموشاندن اعلان‌ها از @{name}",
"account.muted": "خموش", "account.muted": "خموش",
"account.never_active": "هرگز", "account.never_active": "هرگز",
"account.posts": "نوشته‌ها", "account.posts": "بوق",
"account.posts_with_replies": "نوشته‌ها و پاسخ‌ها", "account.posts_with_replies": "نوشته‌ها و پاسخ‌ها",
"account.report": "گزارش @{name}", "account.report": "گزارش @{name}",
"account.requested": "منتظر پذیرش. برای لغو درخواست پی‌گیری کلیک کنید", "account.requested": "منتظر پذیرش. برای لغو درخواست پی‌گیری کلیک کنید",
@ -39,6 +42,10 @@
"account.unfollow": "پایان پیگیری", "account.unfollow": "پایان پیگیری",
"account.unmute": "رفع خموشی @{name}", "account.unmute": "رفع خموشی @{name}",
"account.unmute_notifications": "رفع خموشی اعلان‌ها از @{name}", "account.unmute_notifications": "رفع خموشی اعلان‌ها از @{name}",
"account_note.cancel": "Cancel",
"account_note.edit": "Edit",
"account_note.placeholder": "No comment provided",
"account_note.save": "Save",
"alert.rate_limited.message": "لطفاً پس از {retry_time, time, medium} دوباره بیازمایید.", "alert.rate_limited.message": "لطفاً پس از {retry_time, time, medium} دوباره بیازمایید.",
"alert.rate_limited.title": "محدودیت تعداد", "alert.rate_limited.title": "محدودیت تعداد",
"alert.unexpected.message": "خطایی غیرمنتظره رخ داد.", "alert.unexpected.message": "خطایی غیرمنتظره رخ داد.",
@ -74,9 +81,9 @@
"column_header.show_settings": "نمایش تنظیمات", "column_header.show_settings": "نمایش تنظیمات",
"column_header.unpin": "رهاکردن", "column_header.unpin": "رهاکردن",
"column_subheading.settings": "تنظیمات", "column_subheading.settings": "تنظیمات",
"community.column_settings.local_only": "Local only", "community.column_settings.local_only": "تنها محلّی",
"community.column_settings.media_only": "فقط رسانه", "community.column_settings.media_only": "فقط رسانه",
"community.column_settings.remote_only": "Remote only", "community.column_settings.remote_only": "تنها دوردست",
"compose_form.direct_message_warning": "این بوق تنها به کاربرانی که از آن‌ها نام برده شده فرستاده خواهد شد.", "compose_form.direct_message_warning": "این بوق تنها به کاربرانی که از آن‌ها نام برده شده فرستاده خواهد شد.",
"compose_form.direct_message_warning_learn_more": "بیشتر بدانید", "compose_form.direct_message_warning_learn_more": "بیشتر بدانید",
"compose_form.hashtag_warning": "از آن‌جا که این بوق فهرست‌نشده است، در نتایج جست‌وجوی هشتگ‌ها پیدا نخواهد شد. تنها بوق‌های عمومی را می‌توان با جست‌وجوی هشتگ یافت.", "compose_form.hashtag_warning": "از آن‌جا که این بوق فهرست‌نشده است، در نتایج جست‌وجوی هشتگ‌ها پیدا نخواهد شد. تنها بوق‌های عمومی را می‌توان با جست‌وجوی هشتگ یافت.",
@ -107,7 +114,7 @@
"confirmations.delete_list.message": "مطمئنید می‌خواهید این فهرست را برای همیشه پاک کنید؟", "confirmations.delete_list.message": "مطمئنید می‌خواهید این فهرست را برای همیشه پاک کنید؟",
"confirmations.domain_block.confirm": "نهفتن تمام دامنه", "confirmations.domain_block.confirm": "نهفتن تمام دامنه",
"confirmations.domain_block.message": "آیا جدی جدی می‌خواهید تمام دامنهٔ {domain} را مسدود کنید؟ در بیشتر موارد مسدودسازی یا خموشاندن چند حساب خاص کافی است و توصیه می‌شود. پس از این کار شما هیچ نوشته‌ای را از این دامنه در فهرست نوشته‌های عمومی یا اعلان‌هایتان نخواهید دید. پیگیرانتان از این دامنه هم حذف خواهند شد.", "confirmations.domain_block.message": "آیا جدی جدی می‌خواهید تمام دامنهٔ {domain} را مسدود کنید؟ در بیشتر موارد مسدودسازی یا خموشاندن چند حساب خاص کافی است و توصیه می‌شود. پس از این کار شما هیچ نوشته‌ای را از این دامنه در فهرست نوشته‌های عمومی یا اعلان‌هایتان نخواهید دید. پیگیرانتان از این دامنه هم حذف خواهند شد.",
"confirmations.logout.confirm": "خروج", "confirmations.logout.confirm": "خروج از حساب",
"confirmations.logout.message": "مطمئنید می‌خواهید خارج شوید؟", "confirmations.logout.message": "مطمئنید می‌خواهید خارج شوید؟",
"confirmations.mute.confirm": "خموشاندن", "confirmations.mute.confirm": "خموشاندن",
"confirmations.mute.explanation": "این کار فرسته‌های آن‌ها و فرسته‌هایی را که از آن‌ها نام برده پنهان می‌کند، ولی آن‌ها همچنان اجازه دارند فرسته‌های شما را ببینند و شما را پی بگیرند.", "confirmations.mute.explanation": "این کار فرسته‌های آن‌ها و فرسته‌هایی را که از آن‌ها نام برده پنهان می‌کند، ولی آن‌ها همچنان اجازه دارند فرسته‌های شما را ببینند و شما را پی بگیرند.",
@ -143,7 +150,7 @@
"emoji_button.symbols": "نمادها", "emoji_button.symbols": "نمادها",
"emoji_button.travel": "سفر و مکان", "emoji_button.travel": "سفر و مکان",
"empty_column.account_timeline": "هیچ بوقی این‌جا نیست!", "empty_column.account_timeline": "هیچ بوقی این‌جا نیست!",
"empty_column.account_unavailable": "نمایهٔ ناموجود", "empty_column.account_unavailable": "نمایهٔ موجود نیست",
"empty_column.blocks": "هنوز کسی را مسدود نکرده‌اید.", "empty_column.blocks": "هنوز کسی را مسدود نکرده‌اید.",
"empty_column.bookmarked_statuses": "هنوز هیچ بوق نشان‌شده‌ای ندارید. وقتی بوقی را نشان‌کنید، این‌جا دیده خواهد شد.", "empty_column.bookmarked_statuses": "هنوز هیچ بوق نشان‌شده‌ای ندارید. وقتی بوقی را نشان‌کنید، این‌جا دیده خواهد شد.",
"empty_column.community": "فهرست نوشته‌های محلی خالی است. چیزی بنویسید تا چرخش بچرخد!", "empty_column.community": "فهرست نوشته‌های محلی خالی است. چیزی بنویسید تا چرخش بچرخد!",
@ -166,9 +173,9 @@
"errors.unexpected_crash.report_issue": "گزارش مشکل", "errors.unexpected_crash.report_issue": "گزارش مشکل",
"follow_request.authorize": "اجازه دهید", "follow_request.authorize": "اجازه دهید",
"follow_request.reject": "رد کنید", "follow_request.reject": "رد کنید",
"follow_requests.unlocked_explanation": "با یان که حسابتان قفل نیست، کارکنان {domain} فکر کردند که ممکن است بخواهید درخواست‌ها از این حساب‌ها را به صورت دستی بازبینی کنید.", "follow_requests.unlocked_explanation": "با این که حسابتان قفل نیست، کارکنان {domain} فکر کردند که ممکن است بخواهید درخواست‌ها از این حساب‌ها را به صورت دستی بازبینی کنید.",
"getting_started.developers": "توسعه‌دهندگان", "getting_started.developers": "توسعه‌دهندگان",
"getting_started.directory": "فهرست گزیدهٔ کاربران", "getting_started.directory": "فهرست نمایه",
"getting_started.documentation": "مستندات", "getting_started.documentation": "مستندات",
"getting_started.heading": "آغاز کنید", "getting_started.heading": "آغاز کنید",
"getting_started.invite": "دعوت از دیگران", "getting_started.invite": "دعوت از دیگران",
@ -179,7 +186,7 @@
"hashtag.column_header.tag_mode.any": "یا {additional}", "hashtag.column_header.tag_mode.any": "یا {additional}",
"hashtag.column_header.tag_mode.none": "بدون {additional}", "hashtag.column_header.tag_mode.none": "بدون {additional}",
"hashtag.column_settings.select.no_options_message": "هیچ پیشنهادی پیدا نشد", "hashtag.column_settings.select.no_options_message": "هیچ پیشنهادی پیدا نشد",
"hashtag.column_settings.select.placeholder": "برچسبها را وارد کنید…", "hashtag.column_settings.select.placeholder": "هشتگها را وارد کنید…",
"hashtag.column_settings.tag_mode.all": "همهٔ این‌ها", "hashtag.column_settings.tag_mode.all": "همهٔ این‌ها",
"hashtag.column_settings.tag_mode.any": "هرکدام از این‌ها", "hashtag.column_settings.tag_mode.any": "هرکدام از این‌ها",
"hashtag.column_settings.tag_mode.none": "هیچ‌کدام از این‌ها", "hashtag.column_settings.tag_mode.none": "هیچ‌کدام از این‌ها",
@ -217,14 +224,14 @@
"keyboard_shortcuts.description": "توضیح", "keyboard_shortcuts.description": "توضیح",
"keyboard_shortcuts.direct": "برای گشودن ستون پیغام‌های مستقیم", "keyboard_shortcuts.direct": "برای گشودن ستون پیغام‌های مستقیم",
"keyboard_shortcuts.down": "برای پایین رفتن در فهرست", "keyboard_shortcuts.down": "برای پایین رفتن در فهرست",
"keyboard_shortcuts.enter": "برای گشودن نوشته", "keyboard_shortcuts.enter": "برای گشودن وضعیت",
"keyboard_shortcuts.favourite": "برای پسندیدن", "keyboard_shortcuts.favourite": "برای پسندیدن",
"keyboard_shortcuts.favourites": "برای گشودن فهرست پسندیده‌ها", "keyboard_shortcuts.favourites": "برای گشودن فهرست پسندیده‌ها",
"keyboard_shortcuts.federated": "برای گشودن فهرست نوشته‌های همه‌جا", "keyboard_shortcuts.federated": "برای گشودن فهرست نوشته‌های همه‌جا",
"keyboard_shortcuts.heading": "میان‌برهای صفحه‌کلید", "keyboard_shortcuts.heading": "میان‌برهای صفحه‌کلید",
"keyboard_shortcuts.home": "برای گشودن ستون اصلی پیگیری‌ها", "keyboard_shortcuts.home": "برای گشودن ستون اصلی پیگیری‌ها",
"keyboard_shortcuts.hotkey": "میان‌بر", "keyboard_shortcuts.hotkey": "میان‌بر",
"keyboard_shortcuts.legend": "برای نمایش این راهنما", "keyboard_shortcuts.legend": "برای نمایش این نشانه",
"keyboard_shortcuts.local": "برای گشودن فهرست نوشته‌های محلی", "keyboard_shortcuts.local": "برای گشودن فهرست نوشته‌های محلی",
"keyboard_shortcuts.mention": "برای نام‌بردن از نویسنده", "keyboard_shortcuts.mention": "برای نام‌بردن از نویسنده",
"keyboard_shortcuts.muted": "برای گشودن فهرست کاربران خموش", "keyboard_shortcuts.muted": "برای گشودن فهرست کاربران خموش",
@ -236,6 +243,7 @@
"keyboard_shortcuts.reply": "برای پاسخ", "keyboard_shortcuts.reply": "برای پاسخ",
"keyboard_shortcuts.requests": "برای گشودن فهرست درخواست‌های پیگیری", "keyboard_shortcuts.requests": "برای گشودن فهرست درخواست‌های پیگیری",
"keyboard_shortcuts.search": "برای تمرکز روی جستجو", "keyboard_shortcuts.search": "برای تمرکز روی جستجو",
"keyboard_shortcuts.spoilers": "نمایش/نهفتن زمینهٔ هشدار محتوا",
"keyboard_shortcuts.start": "برای گشودن ستون «آغاز کنید»", "keyboard_shortcuts.start": "برای گشودن ستون «آغاز کنید»",
"keyboard_shortcuts.toggle_hidden": "برای نمایش/نهفتن نوشتهٔ پشت هشدار محتوا", "keyboard_shortcuts.toggle_hidden": "برای نمایش/نهفتن نوشتهٔ پشت هشدار محتوا",
"keyboard_shortcuts.toggle_sensitivity": "برای نمایش/نهفتن رسانه", "keyboard_shortcuts.toggle_sensitivity": "برای نمایش/نهفتن رسانه",
@ -284,13 +292,13 @@
"navigation_bar.preferences": "ترجیحات", "navigation_bar.preferences": "ترجیحات",
"navigation_bar.public_timeline": "نوشته‌های همه‌جا", "navigation_bar.public_timeline": "نوشته‌های همه‌جا",
"navigation_bar.security": "امنیت", "navigation_bar.security": "امنیت",
"notification.favourite": "{name} نوشتهٔ شما را پسندید", "notification.favourite": "{name} وضعیتتان را برگزید",
"notification.follow": "{name} پیگیرتان شد", "notification.follow": "{name} پیگیرتان شد",
"notification.follow_request": "{name} می‌خواهد پیگیر شما باشد", "notification.follow_request": "{name} می‌خواهد پیگیر شما باشد",
"notification.mention": "{name} از شما نام برد", "notification.mention": "{name} از شما نام برد",
"notification.own_poll": "نظرسنجی شما به پایان رسید", "notification.own_poll": "نظرسنجی شما به پایان رسید",
"notification.poll": "نظرسنجی‌ای که در آن رأی دادید به پایان رسیده است", "notification.poll": "نظرسنجی‌ای که در آن رأی دادید به پایان رسیده است",
"notification.reblog": "{name} نوشتهٔ شما را بازبوقید", "notification.reblog": "{name} وضعیتتان را تقویت کرد",
"notifications.clear": "پاک‌کردن اعلان‌ها", "notifications.clear": "پاک‌کردن اعلان‌ها",
"notifications.clear_confirmation": "مطمئنید می‌خواهید همهٔ اعلان‌هایتان را برای همیشه پاک کنید؟", "notifications.clear_confirmation": "مطمئنید می‌خواهید همهٔ اعلان‌هایتان را برای همیشه پاک کنید؟",
"notifications.column_settings.alert": "اعلان‌های میزکار", "notifications.column_settings.alert": "اعلان‌های میزکار",
@ -348,13 +356,13 @@
"report.target": "در حال گزارش {target}", "report.target": "در حال گزارش {target}",
"search.placeholder": "جستجو", "search.placeholder": "جستجو",
"search_popout.search_format": "راهنمای جستجوی پیشرفته", "search_popout.search_format": "راهنمای جستجوی پیشرفته",
"search_popout.tips.full_text": "جستجوی متنی ساده می‌تواند بوق‌هایی که شما نوشته‌اید، پسندیده‌اید، بازبوقیده‌اید، یا در آن‌ها از شما نام برده شده است را پیدا کند. همچنین نام‌های کاربری، نام نمایش‌یافته، و هشتگ‌ها را هم شامل می‌شود.", "search_popout.tips.full_text": "جست‌وجوی متنی ساده وضعیت‌هایی که که نوشته، برگزیده، تقویت‌کرده یا در آن‌ها اشاره‌شده‌اید را به اضافهٔ نام‌های کاربری، نام‌های نمایشی و برچسب‌های مطابق برمی‌گرداند.",
"search_popout.tips.hashtag": "برچسب", "search_popout.tips.hashtag": "هشتگ",
"search_popout.tips.status": "بوق", "search_popout.tips.status": "بوق",
"search_popout.tips.text": "جستجوی متنی ساده برای نام‌ها، نام‌های کاربری، و برچسب‌ها", "search_popout.tips.text": "جستجوی متنی ساده برای نام‌ها، نام‌های کاربری، و برچسب‌ها",
"search_popout.tips.user": "کاربر", "search_popout.tips.user": "کاربر",
"search_results.accounts": "افراد", "search_results.accounts": "افراد",
"search_results.hashtags": "برچسبها", "search_results.hashtags": "هشتگها",
"search_results.statuses": "بوق‌ها", "search_results.statuses": "بوق‌ها",
"search_results.statuses_fts_disabled": "جستجوی محتوای بوق‌ها در این کارساز ماستودون فعال نشده است.", "search_results.statuses_fts_disabled": "جستجوی محتوای بوق‌ها در این کارساز ماستودون فعال نشده است.",
"search_results.total": "{count, number} {count, plural, one {نتیجه} other {نتیجه}}", "search_results.total": "{count, number} {count, plural, one {نتیجه} other {نتیجه}}",
@ -404,7 +412,7 @@
"suggestions.header": "شاید این هم برایتان جالب باشد…", "suggestions.header": "شاید این هم برایتان جالب باشد…",
"tabs_bar.federated_timeline": "همگانی", "tabs_bar.federated_timeline": "همگانی",
"tabs_bar.home": "خانه", "tabs_bar.home": "خانه",
"tabs_bar.local_timeline": "محلّی", "tabs_bar.local_timeline": "بومی",
"tabs_bar.notifications": "اعلان‌ها", "tabs_bar.notifications": "اعلان‌ها",
"tabs_bar.search": "جستجو", "tabs_bar.search": "جستجو",
"time_remaining.days": "{number, plural, one {# روز} other {# روز}} باقی مانده", "time_remaining.days": "{number, plural, one {# روز} other {# روز}} باقی مانده",
@ -412,6 +420,10 @@
"time_remaining.minutes": "{number, plural, one {# دقیقه} other {# دقیقه}} باقی مانده", "time_remaining.minutes": "{number, plural, one {# دقیقه} other {# دقیقه}} باقی مانده",
"time_remaining.moments": "زمان باقی‌مانده", "time_remaining.moments": "زمان باقی‌مانده",
"time_remaining.seconds": "{number, plural, one {# ثانیه} other {# ثانیه}} باقی مانده", "time_remaining.seconds": "{number, plural, one {# ثانیه} other {# ثانیه}} باقی مانده",
"timeline_hint.remote_resource_not_displayed": "{resource} از دیگر کارسازها نمایش داده نمی‌شوند.",
"timeline_hint.resources.followers": "پی‌گیر",
"timeline_hint.resources.follows": "پی می‌گیرد",
"timeline_hint.resources.statuses": "بوق‌های قدیمی‌تر",
"trends.count_by_accounts": "{count} {rawCount, plural, one {نفر نوشته است} other {نفر نوشته‌اند}}", "trends.count_by_accounts": "{count} {rawCount, plural, one {نفر نوشته است} other {نفر نوشته‌اند}}",
"trends.trending_now": "پرطرفدار", "trends.trending_now": "پرطرفدار",
"ui.beforeunload": "اگر از ماستودون خارج شوید پیش‌نویس شما از دست خواهد رفت.", "ui.beforeunload": "اگر از ماستودون خارج شوید پیش‌نویس شما از دست خواهد رفت.",

View File

@ -1,10 +1,13 @@
{ {
"account.account_note_header": "Your note for @{name}",
"account.add_account_note": "Add note for @{name}",
"account.add_or_remove_from_list": "Lisää tai poista listoilta", "account.add_or_remove_from_list": "Lisää tai poista listoilta",
"account.badges.bot": "Botti", "account.badges.bot": "Botti",
"account.badges.group": "Ryhmä", "account.badges.group": "Ryhmä",
"account.block": "Estä @{name}", "account.block": "Estä @{name}",
"account.block_domain": "Piilota kaikki sisältö verkkotunnuksesta {domain}", "account.block_domain": "Piilota kaikki sisältö verkkotunnuksesta {domain}",
"account.blocked": "Estetty", "account.blocked": "Estetty",
"account.browse_more_on_origin_server": "Browse more on the original profile",
"account.cancel_follow_request": "Peruuta seurauspyyntö", "account.cancel_follow_request": "Peruuta seurauspyyntö",
"account.direct": "Viesti käyttäjälle @{name}", "account.direct": "Viesti käyttäjälle @{name}",
"account.domain_blocked": "Verkko-osoite piilotettu", "account.domain_blocked": "Verkko-osoite piilotettu",
@ -39,6 +42,10 @@
"account.unfollow": "Lakkaa seuraamasta", "account.unfollow": "Lakkaa seuraamasta",
"account.unmute": "Poista käyttäjän @{name} mykistys", "account.unmute": "Poista käyttäjän @{name} mykistys",
"account.unmute_notifications": "Poista mykistys käyttäjän @{name} ilmoituksilta", "account.unmute_notifications": "Poista mykistys käyttäjän @{name} ilmoituksilta",
"account_note.cancel": "Cancel",
"account_note.edit": "Edit",
"account_note.placeholder": "No comment provided",
"account_note.save": "Save",
"alert.rate_limited.message": "Yritä uudestaan {retry_time, time, medium} jälkeen.", "alert.rate_limited.message": "Yritä uudestaan {retry_time, time, medium} jälkeen.",
"alert.rate_limited.title": "Määrää rajoitettu", "alert.rate_limited.title": "Määrää rajoitettu",
"alert.unexpected.message": "Tapahtui odottamaton virhe.", "alert.unexpected.message": "Tapahtui odottamaton virhe.",
@ -236,6 +243,7 @@
"keyboard_shortcuts.reply": "vastaa", "keyboard_shortcuts.reply": "vastaa",
"keyboard_shortcuts.requests": "avaa lista seurauspyynnöistä", "keyboard_shortcuts.requests": "avaa lista seurauspyynnöistä",
"keyboard_shortcuts.search": "siirry hakukenttään", "keyboard_shortcuts.search": "siirry hakukenttään",
"keyboard_shortcuts.spoilers": "to show/hide CW field",
"keyboard_shortcuts.start": "avaa \"Aloitus\" -sarake", "keyboard_shortcuts.start": "avaa \"Aloitus\" -sarake",
"keyboard_shortcuts.toggle_hidden": "näytä/piilota sisältövaroituksella merkitty teksti", "keyboard_shortcuts.toggle_hidden": "näytä/piilota sisältövaroituksella merkitty teksti",
"keyboard_shortcuts.toggle_sensitivity": "näytä/piilota media", "keyboard_shortcuts.toggle_sensitivity": "näytä/piilota media",
@ -412,6 +420,10 @@
"time_remaining.minutes": "{number, plural, one {# minuutti} other {# minuuttia}} jäljellä", "time_remaining.minutes": "{number, plural, one {# minuutti} other {# minuuttia}} jäljellä",
"time_remaining.moments": "Hetki jäljellä", "time_remaining.moments": "Hetki jäljellä",
"time_remaining.seconds": "{number, plural, one {# sekunti} other {# sekuntia}} jäljellä", "time_remaining.seconds": "{number, plural, one {# sekunti} other {# sekuntia}} jäljellä",
"timeline_hint.remote_resource_not_displayed": "{resource} from other servers are not displayed.",
"timeline_hint.resources.followers": "Followers",
"timeline_hint.resources.follows": "Follows",
"timeline_hint.resources.statuses": "Older toots",
"trends.count_by_accounts": "{count} {rawCount, plural, one {henkilö} other {henkilöä}} keskustelee", "trends.count_by_accounts": "{count} {rawCount, plural, one {henkilö} other {henkilöä}} keskustelee",
"trends.trending_now": "Suosittua nyt", "trends.trending_now": "Suosittua nyt",
"ui.beforeunload": "Luonnos häviää, jos poistut Mastodonista.", "ui.beforeunload": "Luonnos häviää, jos poistut Mastodonista.",

View File

@ -1,10 +1,13 @@
{ {
"account.account_note_header": "Your note for @{name}",
"account.add_account_note": "Add note for @{name}",
"account.add_or_remove_from_list": "Ajouter ou retirer des listes", "account.add_or_remove_from_list": "Ajouter ou retirer des listes",
"account.badges.bot": "Robot", "account.badges.bot": "Robot",
"account.badges.group": "Groupe", "account.badges.group": "Groupe",
"account.block": "Bloquer @{name}", "account.block": "Bloquer @{name}",
"account.block_domain": "Bloquer le domaine {domain}", "account.block_domain": "Bloquer le domaine {domain}",
"account.blocked": "Bloqué·e", "account.blocked": "Bloqué·e",
"account.browse_more_on_origin_server": "Parcourir davantage sur le profil original",
"account.cancel_follow_request": "Annuler la demande de suivi", "account.cancel_follow_request": "Annuler la demande de suivi",
"account.direct": "Envoyer un message direct à @{name}", "account.direct": "Envoyer un message direct à @{name}",
"account.domain_blocked": "Domaine bloqué", "account.domain_blocked": "Domaine bloqué",
@ -39,6 +42,10 @@
"account.unfollow": "Ne plus suivre", "account.unfollow": "Ne plus suivre",
"account.unmute": "Ne plus masquer @{name}", "account.unmute": "Ne plus masquer @{name}",
"account.unmute_notifications": "Ne plus masquer les notifications de @{name}", "account.unmute_notifications": "Ne plus masquer les notifications de @{name}",
"account_note.cancel": "Cancel",
"account_note.edit": "Edit",
"account_note.placeholder": "No comment provided",
"account_note.save": "Save",
"alert.rate_limited.message": "Veuillez réessayer après {retry_time, time, medium}.", "alert.rate_limited.message": "Veuillez réessayer après {retry_time, time, medium}.",
"alert.rate_limited.title": "Taux limité", "alert.rate_limited.title": "Taux limité",
"alert.unexpected.message": "Une erreur inattendue sest produite.", "alert.unexpected.message": "Une erreur inattendue sest produite.",
@ -74,9 +81,9 @@
"column_header.show_settings": "Afficher les paramètres", "column_header.show_settings": "Afficher les paramètres",
"column_header.unpin": "Désépingler", "column_header.unpin": "Désépingler",
"column_subheading.settings": "Paramètres", "column_subheading.settings": "Paramètres",
"community.column_settings.local_only": "Local only", "community.column_settings.local_only": "Local seulement",
"community.column_settings.media_only": "Média uniquement", "community.column_settings.media_only": "Média uniquement",
"community.column_settings.remote_only": "Remote only", "community.column_settings.remote_only": "Distant seulement",
"compose_form.direct_message_warning": "Ce pouet sera uniquement envoyé aux personnes mentionnées. Cependant, ladministration de votre instance et des instances réceptrices pourront inspecter ce message.", "compose_form.direct_message_warning": "Ce pouet sera uniquement envoyé aux personnes mentionnées. Cependant, ladministration de votre instance et des instances réceptrices pourront inspecter ce message.",
"compose_form.direct_message_warning_learn_more": "En savoir plus", "compose_form.direct_message_warning_learn_more": "En savoir plus",
"compose_form.hashtag_warning": "Ce pouet ne sera pas listé dans les recherches par hashtag car sa visibilité est réglée sur « non listé ». Seuls les pouets avec une visibilité « publique » peuvent être recherchés par hashtag.", "compose_form.hashtag_warning": "Ce pouet ne sera pas listé dans les recherches par hashtag car sa visibilité est réglée sur « non listé ». Seuls les pouets avec une visibilité « publique » peuvent être recherchés par hashtag.",
@ -102,7 +109,7 @@
"confirmations.block.confirm": "Bloquer", "confirmations.block.confirm": "Bloquer",
"confirmations.block.message": "Voulez-vous vraiment bloquer {name}?", "confirmations.block.message": "Voulez-vous vraiment bloquer {name}?",
"confirmations.delete.confirm": "Supprimer", "confirmations.delete.confirm": "Supprimer",
"confirmations.delete.message": "Voulez-vous vraiment supprimer ce pouet?", "confirmations.delete.message": "Confirmez-vous la suppression de ce pouet?",
"confirmations.delete_list.confirm": "Supprimer", "confirmations.delete_list.confirm": "Supprimer",
"confirmations.delete_list.message": "Voulez-vous vraiment supprimer définitivement cette liste?", "confirmations.delete_list.message": "Voulez-vous vraiment supprimer définitivement cette liste?",
"confirmations.domain_block.confirm": "Bloquer tout le domaine", "confirmations.domain_block.confirm": "Bloquer tout le domaine",
@ -113,7 +120,7 @@
"confirmations.mute.explanation": "Cela masquera ses messages et les messages le ou la mentionnant, mais cela lui permettra quand même de voir vos messages et de vous suivre.", "confirmations.mute.explanation": "Cela masquera ses messages et les messages le ou la mentionnant, mais cela lui permettra quand même de voir vos messages et de vous suivre.",
"confirmations.mute.message": "Voulez-vous vraiment masquer {name} ?", "confirmations.mute.message": "Voulez-vous vraiment masquer {name} ?",
"confirmations.redraft.confirm": "Supprimer et ré-écrire", "confirmations.redraft.confirm": "Supprimer et ré-écrire",
"confirmations.redraft.message": "Voulez-vous vraiment supprimer ce pouet pour le ré-écrire? Ses partages ainsi que ses mises en favori seront perdu·e·s et ses réponses seront orphelines.", "confirmations.redraft.message": "Êtes-vous sûr·e de vouloir effacer ce statut pour le ré-écrire? Ses partages ainsi que ses mises en favori seront perdu·e·s et ses réponses seront orphelines.",
"confirmations.reply.confirm": "Répondre", "confirmations.reply.confirm": "Répondre",
"confirmations.reply.message": "Répondre maintenant écrasera le message que vous rédigez actuellement. Voulez-vous vraiment continuer ?", "confirmations.reply.message": "Répondre maintenant écrasera le message que vous rédigez actuellement. Voulez-vous vraiment continuer ?",
"confirmations.unfollow.confirm": "Ne plus suivre", "confirmations.unfollow.confirm": "Ne plus suivre",
@ -212,12 +219,12 @@
"keyboard_shortcuts.back": "revenir en arrière", "keyboard_shortcuts.back": "revenir en arrière",
"keyboard_shortcuts.blocked": "ouvrir la liste des comptes bloqués", "keyboard_shortcuts.blocked": "ouvrir la liste des comptes bloqués",
"keyboard_shortcuts.boost": "partager", "keyboard_shortcuts.boost": "partager",
"keyboard_shortcuts.column": "cibler un pouet dune des colonnes", "keyboard_shortcuts.column": "pour focaliser un statut dans lune des colonnes",
"keyboard_shortcuts.compose": "cibler la zone de rédaction", "keyboard_shortcuts.compose": "cibler la zone de rédaction",
"keyboard_shortcuts.description": "Description", "keyboard_shortcuts.description": "Description",
"keyboard_shortcuts.direct": "ouvrir la colonne des messages directs", "keyboard_shortcuts.direct": "ouvrir la colonne des messages directs",
"keyboard_shortcuts.down": "descendre dans la liste", "keyboard_shortcuts.down": "descendre dans la liste",
"keyboard_shortcuts.enter": "ouvrir le pouet", "keyboard_shortcuts.enter": "pour ouvrir le statut",
"keyboard_shortcuts.favourite": "ajouter aux favoris", "keyboard_shortcuts.favourite": "ajouter aux favoris",
"keyboard_shortcuts.favourites": "ouvrir la liste des favoris", "keyboard_shortcuts.favourites": "ouvrir la liste des favoris",
"keyboard_shortcuts.federated": "ouvrir le fil public global", "keyboard_shortcuts.federated": "ouvrir le fil public global",
@ -236,6 +243,7 @@
"keyboard_shortcuts.reply": "répondre", "keyboard_shortcuts.reply": "répondre",
"keyboard_shortcuts.requests": "ouvrir la liste de demandes dabonnement", "keyboard_shortcuts.requests": "ouvrir la liste de demandes dabonnement",
"keyboard_shortcuts.search": "cibler la zone de recherche", "keyboard_shortcuts.search": "cibler la zone de recherche",
"keyboard_shortcuts.spoilers": "pour afficher/masquer le champ CW",
"keyboard_shortcuts.start": "ouvrir la colonne « Pour commencer »", "keyboard_shortcuts.start": "ouvrir la colonne « Pour commencer »",
"keyboard_shortcuts.toggle_hidden": "déplier/replier le texte derrière un CW", "keyboard_shortcuts.toggle_hidden": "déplier/replier le texte derrière un CW",
"keyboard_shortcuts.toggle_sensitivity": "afficher/cacher les médias", "keyboard_shortcuts.toggle_sensitivity": "afficher/cacher les médias",
@ -322,13 +330,13 @@
"poll_button.add_poll": "Ajouter un sondage", "poll_button.add_poll": "Ajouter un sondage",
"poll_button.remove_poll": "Supprimer le sondage", "poll_button.remove_poll": "Supprimer le sondage",
"privacy.change": "Ajuster la confidentialité du message", "privacy.change": "Ajuster la confidentialité du message",
"privacy.direct.long": "Nenvoyer quaux personnes mentionnées", "privacy.direct.long": "Visible uniquement par les comptes mentionnés",
"privacy.direct.short": "Direct", "privacy.direct.short": "Direct",
"privacy.private.long": "Seul·e·s vos abonné·e·s verront vos statuts", "privacy.private.long": "Visible uniquement par vos abonné·e·s",
"privacy.private.short": "Abonné·e·s uniquement", "privacy.private.short": "Abonné·e·s uniquement",
"privacy.public.long": "Afficher dans les fils publics", "privacy.public.long": "Visible par tou·te·s, affiché dans les fils publics",
"privacy.public.short": "Public", "privacy.public.short": "Public",
"privacy.unlisted.long": "Ne pas afficher dans les fils publics", "privacy.unlisted.long": "Visible par tou·te·s, mais pas dans les fils publics",
"privacy.unlisted.short": "Non listé", "privacy.unlisted.short": "Non listé",
"refresh": "Actualiser", "refresh": "Actualiser",
"regeneration_indicator.label": "Chargement…", "regeneration_indicator.label": "Chargement…",
@ -350,7 +358,7 @@
"search_popout.search_format": "Recherche avancée", "search_popout.search_format": "Recherche avancée",
"search_popout.tips.full_text": "Un texte normal retourne les pouets que vous avez écris, mis en favori, partagés, ou vous mentionnant, ainsi que les identifiants, les noms affichés, et les hashtags des personnes et messages correspondant.", "search_popout.tips.full_text": "Un texte normal retourne les pouets que vous avez écris, mis en favori, partagés, ou vous mentionnant, ainsi que les identifiants, les noms affichés, et les hashtags des personnes et messages correspondant.",
"search_popout.tips.hashtag": "hashtag", "search_popout.tips.hashtag": "hashtag",
"search_popout.tips.status": "pouet", "search_popout.tips.status": "statuts",
"search_popout.tips.text": "Un texte simple renvoie les noms affichés, les identifiants et les hashtags correspondants", "search_popout.tips.text": "Un texte simple renvoie les noms affichés, les identifiants et les hashtags correspondants",
"search_popout.tips.user": "utilisateur·ice", "search_popout.tips.user": "utilisateur·ice",
"search_results.accounts": "Comptes", "search_results.accounts": "Comptes",
@ -359,7 +367,7 @@
"search_results.statuses_fts_disabled": "La recherche de pouets par leur contenu n'est pas activée sur ce serveur Mastodon.", "search_results.statuses_fts_disabled": "La recherche de pouets par leur contenu n'est pas activée sur ce serveur Mastodon.",
"search_results.total": "{count, number} {count, plural, one {résultat} other {résultats}}", "search_results.total": "{count, number} {count, plural, one {résultat} other {résultats}}",
"status.admin_account": "Ouvrir linterface de modération pour @{name}", "status.admin_account": "Ouvrir linterface de modération pour @{name}",
"status.admin_status": "Ouvrir ce pouet dans linterface de modération", "status.admin_status": "Ouvrir ce statut dans linterface de modération",
"status.block": "Bloquer @{name}", "status.block": "Bloquer @{name}",
"status.bookmark": "Ajouter aux marque-pages", "status.bookmark": "Ajouter aux marque-pages",
"status.cancel_reblog_private": "Annuler le partage", "status.cancel_reblog_private": "Annuler le partage",
@ -377,7 +385,7 @@
"status.more": "Plus", "status.more": "Plus",
"status.mute": "Masquer @{name}", "status.mute": "Masquer @{name}",
"status.mute_conversation": "Masquer la conversation", "status.mute_conversation": "Masquer la conversation",
"status.open": "Voir les détails du pouet", "status.open": "Déplier ce statut",
"status.pin": "Épingler sur le profil", "status.pin": "Épingler sur le profil",
"status.pinned": "Pouet épinglé", "status.pinned": "Pouet épinglé",
"status.read_more": "En savoir plus", "status.read_more": "En savoir plus",
@ -412,6 +420,10 @@
"time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} restantes", "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} restantes",
"time_remaining.moments": "Encore quelques instants", "time_remaining.moments": "Encore quelques instants",
"time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} restantes", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} restantes",
"timeline_hint.remote_resource_not_displayed": "{resource} des autres serveurs ne sont pas affichés.",
"timeline_hint.resources.followers": "Les abonnés",
"timeline_hint.resources.follows": "Les abonnements",
"timeline_hint.resources.statuses": "Les anciens pouets",
"trends.count_by_accounts": "{count} {rawCount, plural, one {personne discute} other {personnes discutent}}", "trends.count_by_accounts": "{count} {rawCount, plural, one {personne discute} other {personnes discutent}}",
"trends.trending_now": "Tendance en ce moment", "trends.trending_now": "Tendance en ce moment",
"ui.beforeunload": "Votre brouillon sera perdu si vous quittez Mastodon.", "ui.beforeunload": "Votre brouillon sera perdu si vous quittez Mastodon.",

View File

@ -1,10 +1,13 @@
{ {
"account.account_note_header": "Your note for @{name}",
"account.add_account_note": "Add note for @{name}",
"account.add_or_remove_from_list": "Add or Remove from lists", "account.add_or_remove_from_list": "Add or Remove from lists",
"account.badges.bot": "Bot", "account.badges.bot": "Bot",
"account.badges.group": "Group", "account.badges.group": "Group",
"account.block": "Block @{name}", "account.block": "Block @{name}",
"account.block_domain": "Hide everything from {domain}", "account.block_domain": "Hide everything from {domain}",
"account.blocked": "Blocked", "account.blocked": "Blocked",
"account.browse_more_on_origin_server": "Browse more on the original profile",
"account.cancel_follow_request": "Cancel follow request", "account.cancel_follow_request": "Cancel follow request",
"account.direct": "Direct message @{name}", "account.direct": "Direct message @{name}",
"account.domain_blocked": "Domain hidden", "account.domain_blocked": "Domain hidden",
@ -39,6 +42,10 @@
"account.unfollow": "Unfollow", "account.unfollow": "Unfollow",
"account.unmute": "Unmute @{name}", "account.unmute": "Unmute @{name}",
"account.unmute_notifications": "Unmute notifications from @{name}", "account.unmute_notifications": "Unmute notifications from @{name}",
"account_note.cancel": "Cancel",
"account_note.edit": "Edit",
"account_note.placeholder": "No comment provided",
"account_note.save": "Save",
"alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.",
"alert.rate_limited.title": "Rate limited", "alert.rate_limited.title": "Rate limited",
"alert.unexpected.message": "An unexpected error occurred.", "alert.unexpected.message": "An unexpected error occurred.",
@ -236,6 +243,7 @@
"keyboard_shortcuts.reply": "to reply", "keyboard_shortcuts.reply": "to reply",
"keyboard_shortcuts.requests": "to open follow requests list", "keyboard_shortcuts.requests": "to open follow requests list",
"keyboard_shortcuts.search": "to focus search", "keyboard_shortcuts.search": "to focus search",
"keyboard_shortcuts.spoilers": "to show/hide CW field",
"keyboard_shortcuts.start": "to open \"get started\" column", "keyboard_shortcuts.start": "to open \"get started\" column",
"keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toggle_sensitivity": "to show/hide media", "keyboard_shortcuts.toggle_sensitivity": "to show/hide media",
@ -257,7 +265,7 @@
"lists.subheading": "Your lists", "lists.subheading": "Your lists",
"load_pending": "{count, plural, one {# new item} other {# new items}}", "load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Loading...", "loading_indicator.label": "Loading...",
"media_gallery.toggle_visible": "Hide {number, plural, one {image} other {images}}", "media_gallery.toggle_visible": "Hide media",
"missing_indicator.label": "Not found", "missing_indicator.label": "Not found",
"missing_indicator.sublabel": "This resource could not be found", "missing_indicator.sublabel": "This resource could not be found",
"mute_modal.hide_notifications": "Hide notifications from this user?", "mute_modal.hide_notifications": "Hide notifications from this user?",
@ -412,6 +420,10 @@
"time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
"time_remaining.moments": "Moments remaining", "time_remaining.moments": "Moments remaining",
"time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
"timeline_hint.remote_resource_not_displayed": "{resource} from other servers are not displayed.",
"timeline_hint.resources.followers": "Followers",
"timeline_hint.resources.follows": "Follows",
"timeline_hint.resources.statuses": "Older toots",
"trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
"trends.trending_now": "Trending now", "trends.trending_now": "Trending now",
"ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",

View File

@ -1,10 +1,13 @@
{ {
"account.account_note_header": "Your note for @{name}",
"account.add_account_note": "Add note for @{name}",
"account.add_or_remove_from_list": "Engadir ou eliminar das listaxes", "account.add_or_remove_from_list": "Engadir ou eliminar das listaxes",
"account.badges.bot": "Bot", "account.badges.bot": "Bot",
"account.badges.group": "Grupo", "account.badges.group": "Grupo",
"account.block": "Bloquear @{name}", "account.block": "Bloquear @{name}",
"account.block_domain": "Agochar todo de {domain}", "account.block_domain": "Agochar todo de {domain}",
"account.blocked": "Bloqueada", "account.blocked": "Bloqueada",
"account.browse_more_on_origin_server": "Busca máis no perfil orixinal",
"account.cancel_follow_request": "Desbotar solicitude de seguimento", "account.cancel_follow_request": "Desbotar solicitude de seguimento",
"account.direct": "Mensaxe directa @{name}", "account.direct": "Mensaxe directa @{name}",
"account.domain_blocked": "Dominio agochado", "account.domain_blocked": "Dominio agochado",
@ -39,6 +42,10 @@
"account.unfollow": "Deixar de seguir", "account.unfollow": "Deixar de seguir",
"account.unmute": "Deixar de silenciar a @{name}", "account.unmute": "Deixar de silenciar a @{name}",
"account.unmute_notifications": "Deixar de silenciar as notificacións de @{name}", "account.unmute_notifications": "Deixar de silenciar as notificacións de @{name}",
"account_note.cancel": "Cancel",
"account_note.edit": "Edit",
"account_note.placeholder": "No comment provided",
"account_note.save": "Save",
"alert.rate_limited.message": "Téntao novamente após {retry_time, time, medium}.", "alert.rate_limited.message": "Téntao novamente após {retry_time, time, medium}.",
"alert.rate_limited.title": "Límite de intentos", "alert.rate_limited.title": "Límite de intentos",
"alert.unexpected.message": "Ocorreu un erro non agardado.", "alert.unexpected.message": "Ocorreu un erro non agardado.",
@ -74,9 +81,9 @@
"column_header.show_settings": "Amosar axustes", "column_header.show_settings": "Amosar axustes",
"column_header.unpin": "Desapegar", "column_header.unpin": "Desapegar",
"column_subheading.settings": "Axustes", "column_subheading.settings": "Axustes",
"community.column_settings.local_only": "Local only", "community.column_settings.local_only": "Só local",
"community.column_settings.media_only": "Só multimedia", "community.column_settings.media_only": "Só multimedia",
"community.column_settings.remote_only": "Remote only", "community.column_settings.remote_only": "Só remoto",
"compose_form.direct_message_warning": "Este toot só será enviado ás usuarias mencionadas.", "compose_form.direct_message_warning": "Este toot só será enviado ás usuarias mencionadas.",
"compose_form.direct_message_warning_learn_more": "Coñecer máis", "compose_form.direct_message_warning_learn_more": "Coñecer máis",
"compose_form.hashtag_warning": "Este toot non aparecerá baixo ningún cancelo (hashtag) porque non está listado. Só se poden procurar toots públicos por cancelos.", "compose_form.hashtag_warning": "Este toot non aparecerá baixo ningún cancelo (hashtag) porque non está listado. Só se poden procurar toots públicos por cancelos.",
@ -155,7 +162,7 @@
"empty_column.hashtag": "Aínda non hai nada con este cancelo.", "empty_column.hashtag": "Aínda non hai nada con este cancelo.",
"empty_column.home": "A túa cronoloxía inicial está baleira! Visita {public} ou emprega a procura para atopar outras usuarias.", "empty_column.home": "A túa cronoloxía inicial está baleira! Visita {public} ou emprega a procura para atopar outras usuarias.",
"empty_column.home.public_timeline": "a cronoloxía pública", "empty_column.home.public_timeline": "a cronoloxía pública",
"empty_column.list": "Aínda non hai nada en esta lista. Cando as usuarias incluídas na lista publiquen mensaxes, aparecerán aquí.", "empty_column.list": "Aínda non hai nada nesta listaxe. Cando os usuarios incluídas na listaxe publiquen mensaxes, amosaranse aquí.",
"empty_column.lists": "Aínda non tes listaxes. Cando crees unha, amosarase aquí.", "empty_column.lists": "Aínda non tes listaxes. Cando crees unha, amosarase aquí.",
"empty_column.mutes": "Aínda non silenciaches a ningúnha usuaria.", "empty_column.mutes": "Aínda non silenciaches a ningúnha usuaria.",
"empty_column.notifications": "Aínda non tes notificacións. Interactúa con outras para comezar unha conversa.", "empty_column.notifications": "Aínda non tes notificacións. Interactúa con outras para comezar unha conversa.",
@ -236,6 +243,7 @@
"keyboard_shortcuts.reply": "para responder", "keyboard_shortcuts.reply": "para responder",
"keyboard_shortcuts.requests": "para abrir a listaxe das peticións de seguimento", "keyboard_shortcuts.requests": "para abrir a listaxe das peticións de seguimento",
"keyboard_shortcuts.search": "para destacar a procura", "keyboard_shortcuts.search": "para destacar a procura",
"keyboard_shortcuts.spoilers": "mostrar/ocultar campo CW",
"keyboard_shortcuts.start": "para abrir a columna dos \"primeiros pasos\"", "keyboard_shortcuts.start": "para abrir a columna dos \"primeiros pasos\"",
"keyboard_shortcuts.toggle_hidden": "para amosar/agochar texto detrás do aviso de contido (AC)", "keyboard_shortcuts.toggle_hidden": "para amosar/agochar texto detrás do aviso de contido (AC)",
"keyboard_shortcuts.toggle_sensitivity": "para amosar/agochar contido multimedia", "keyboard_shortcuts.toggle_sensitivity": "para amosar/agochar contido multimedia",
@ -290,7 +298,7 @@
"notification.mention": "{name} mencionoute", "notification.mention": "{name} mencionoute",
"notification.own_poll": "A túa enquisa rematou", "notification.own_poll": "A túa enquisa rematou",
"notification.poll": "Unha enquisa na que votaches rematou", "notification.poll": "Unha enquisa na que votaches rematou",
"notification.reblog": "{name} promoveu o teu estado", "notification.reblog": "{name} compartiu o teu estado",
"notifications.clear": "Limpar notificacións", "notifications.clear": "Limpar notificacións",
"notifications.clear_confirmation": "Tes a certeza de querer limpar de xeito permanente todas as túas notificacións?", "notifications.clear_confirmation": "Tes a certeza de querer limpar de xeito permanente todas as túas notificacións?",
"notifications.column_settings.alert": "Notificacións de escritorio", "notifications.column_settings.alert": "Notificacións de escritorio",
@ -412,6 +420,10 @@
"time_remaining.minutes": "{number, plural, one {# minuto} other {# minutos}} restantes", "time_remaining.minutes": "{number, plural, one {# minuto} other {# minutos}} restantes",
"time_remaining.moments": "Momentos restantes", "time_remaining.moments": "Momentos restantes",
"time_remaining.seconds": "{number, plural, one {# segundo} other {# segundos}} restantes", "time_remaining.seconds": "{number, plural, one {# segundo} other {# segundos}} restantes",
"timeline_hint.remote_resource_not_displayed": "Non se mostran {resource} desde outros servidores.",
"timeline_hint.resources.followers": "Seguidoras",
"timeline_hint.resources.follows": "Seguindo",
"timeline_hint.resources.statuses": "Toots antigos",
"trends.count_by_accounts": "{count} {rawCount, plural, one {persoa} other {persoas}} falando", "trends.count_by_accounts": "{count} {rawCount, plural, one {persoa} other {persoas}} falando",
"trends.trending_now": "Tendencias actuais", "trends.trending_now": "Tendencias actuais",
"ui.beforeunload": "O borrador perderase se saes de Mastodon.", "ui.beforeunload": "O borrador perderase se saes de Mastodon.",

View File

@ -1,10 +1,13 @@
{ {
"account.account_note_header": "Your note for @{name}",
"account.add_account_note": "Add note for @{name}",
"account.add_or_remove_from_list": "הוסף או הסר מהרשימות", "account.add_or_remove_from_list": "הוסף או הסר מהרשימות",
"account.badges.bot": "בוט", "account.badges.bot": "בוט",
"account.badges.group": "Group", "account.badges.group": "Group",
"account.block": "חסימת @{name}", "account.block": "חסימת @{name}",
"account.block_domain": "להסתיר הכל מהקהילה {domain}", "account.block_domain": "להסתיר הכל מהקהילה {domain}",
"account.blocked": "חסום", "account.blocked": "חסום",
"account.browse_more_on_origin_server": "Browse more on the original profile",
"account.cancel_follow_request": "בטל בקשת מעקב", "account.cancel_follow_request": "בטל בקשת מעקב",
"account.direct": "Direct Message @{name}", "account.direct": "Direct Message @{name}",
"account.domain_blocked": "הדומיין חסוי", "account.domain_blocked": "הדומיין חסוי",
@ -39,6 +42,10 @@
"account.unfollow": "הפסקת מעקב", "account.unfollow": "הפסקת מעקב",
"account.unmute": "הפסקת השתקת @{name}", "account.unmute": "הפסקת השתקת @{name}",
"account.unmute_notifications": "להפסיק הסתרת הודעות מעם @{name}", "account.unmute_notifications": "להפסיק הסתרת הודעות מעם @{name}",
"account_note.cancel": "Cancel",
"account_note.edit": "Edit",
"account_note.placeholder": "No comment provided",
"account_note.save": "Save",
"alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.",
"alert.rate_limited.title": "Rate limited", "alert.rate_limited.title": "Rate limited",
"alert.unexpected.message": "אירעה שגיאה בלתי צפויה.", "alert.unexpected.message": "אירעה שגיאה בלתי צפויה.",
@ -236,6 +243,7 @@
"keyboard_shortcuts.reply": "לענות", "keyboard_shortcuts.reply": "לענות",
"keyboard_shortcuts.requests": "to open follow requests list", "keyboard_shortcuts.requests": "to open follow requests list",
"keyboard_shortcuts.search": "להתמקד בחלון החיפוש", "keyboard_shortcuts.search": "להתמקד בחלון החיפוש",
"keyboard_shortcuts.spoilers": "to show/hide CW field",
"keyboard_shortcuts.start": "to open \"get started\" column", "keyboard_shortcuts.start": "to open \"get started\" column",
"keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toggle_sensitivity": "to show/hide media", "keyboard_shortcuts.toggle_sensitivity": "to show/hide media",
@ -412,6 +420,10 @@
"time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
"time_remaining.moments": "Moments remaining", "time_remaining.moments": "Moments remaining",
"time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
"timeline_hint.remote_resource_not_displayed": "{resource} from other servers are not displayed.",
"timeline_hint.resources.followers": "Followers",
"timeline_hint.resources.follows": "Follows",
"timeline_hint.resources.statuses": "Older toots",
"trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
"trends.trending_now": "Trending now", "trends.trending_now": "Trending now",
"ui.beforeunload": "הטיוטא תאבד אם תעזבו את מסטודון.", "ui.beforeunload": "הטיוטא תאבד אם תעזבו את מסטודון.",

View File

@ -1,10 +1,13 @@
{ {
"account.account_note_header": "Your note for @{name}",
"account.add_account_note": "Add note for @{name}",
"account.add_or_remove_from_list": "सूची में जोड़ें या हटाए", "account.add_or_remove_from_list": "सूची में जोड़ें या हटाए",
"account.badges.bot": "बॉट", "account.badges.bot": "बॉट",
"account.badges.group": "Group", "account.badges.group": "Group",
"account.block": "@{name} को ब्लॉक करें", "account.block": "@{name} को ब्लॉक करें",
"account.block_domain": "{domain} के सारी चीज़े छुपाएं", "account.block_domain": "{domain} के सारी चीज़े छुपाएं",
"account.blocked": "ब्लॉक", "account.blocked": "ब्लॉक",
"account.browse_more_on_origin_server": "Browse more on the original profile",
"account.cancel_follow_request": "फ़ॉलो रिक्वेस्ट रद्द करें", "account.cancel_follow_request": "फ़ॉलो रिक्वेस्ट रद्द करें",
"account.direct": "प्रत्यक्ष संदेश @{name}", "account.direct": "प्रत्यक्ष संदेश @{name}",
"account.domain_blocked": "छिपा हुआ डोमेन", "account.domain_blocked": "छिपा हुआ डोमेन",
@ -39,6 +42,10 @@
"account.unfollow": "अनफॉलो करें", "account.unfollow": "अनफॉलो करें",
"account.unmute": "अनम्यूट @{name}", "account.unmute": "अनम्यूट @{name}",
"account.unmute_notifications": "@{name} के नोटिफिकेशन अनम्यूट करे", "account.unmute_notifications": "@{name} के नोटिफिकेशन अनम्यूट करे",
"account_note.cancel": "Cancel",
"account_note.edit": "Edit",
"account_note.placeholder": "No comment provided",
"account_note.save": "Save",
"alert.rate_limited.message": "कृप्या {retry_time, time, medium} के बाद दुबारा कोशिश करें", "alert.rate_limited.message": "कृप्या {retry_time, time, medium} के बाद दुबारा कोशिश करें",
"alert.rate_limited.title": "सीमित दर", "alert.rate_limited.title": "सीमित दर",
"alert.unexpected.message": "एक अप्रत्याशित त्रुटि हुई है!", "alert.unexpected.message": "एक अप्रत्याशित त्रुटि हुई है!",
@ -236,6 +243,7 @@
"keyboard_shortcuts.reply": "जवाब के लिए", "keyboard_shortcuts.reply": "जवाब के लिए",
"keyboard_shortcuts.requests": "फॉलो रिक्वेस्ट लिस्ट खोलने के लिए", "keyboard_shortcuts.requests": "फॉलो रिक्वेस्ट लिस्ट खोलने के लिए",
"keyboard_shortcuts.search": "गहरी खोज के लिए", "keyboard_shortcuts.search": "गहरी खोज के लिए",
"keyboard_shortcuts.spoilers": "to show/hide CW field",
"keyboard_shortcuts.start": "to open \"get started\" column", "keyboard_shortcuts.start": "to open \"get started\" column",
"keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toggle_sensitivity": "to show/hide media", "keyboard_shortcuts.toggle_sensitivity": "to show/hide media",
@ -412,11 +420,15 @@
"time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
"time_remaining.moments": "Moments remaining", "time_remaining.moments": "Moments remaining",
"time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
"timeline_hint.remote_resource_not_displayed": "{resource} from other servers are not displayed.",
"timeline_hint.resources.followers": "Followers",
"timeline_hint.resources.follows": "Follows",
"timeline_hint.resources.statuses": "Older toots",
"trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
"trends.trending_now": "Trending now", "trends.trending_now": "Trending now",
"ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
"upload_area.title": "Drag & drop to upload", "upload_area.title": "Drag & drop to upload",
"upload_button.label": "Add media ({formats})", "upload_button.label": "Add images, a video or an audio file",
"upload_error.limit": "File upload limit exceeded.", "upload_error.limit": "File upload limit exceeded.",
"upload_error.poll": "File upload not allowed with polls.", "upload_error.poll": "File upload not allowed with polls.",
"upload_form.audio_description": "Describe for people with hearing loss", "upload_form.audio_description": "Describe for people with hearing loss",

View File

@ -1,10 +1,13 @@
{ {
"account.account_note_header": "Your note for @{name}",
"account.add_account_note": "Add note for @{name}",
"account.add_or_remove_from_list": "Add or Remove from lists", "account.add_or_remove_from_list": "Add or Remove from lists",
"account.badges.bot": "Bot", "account.badges.bot": "Bot",
"account.badges.group": "Group", "account.badges.group": "Group",
"account.block": "Blokiraj @{name}", "account.block": "Blokiraj @{name}",
"account.block_domain": "Sakrij sve sa {domain}", "account.block_domain": "Sakrij sve sa {domain}",
"account.blocked": "Blocked", "account.blocked": "Blocked",
"account.browse_more_on_origin_server": "Browse more on the original profile",
"account.cancel_follow_request": "Cancel follow request", "account.cancel_follow_request": "Cancel follow request",
"account.direct": "Direct Message @{name}", "account.direct": "Direct Message @{name}",
"account.domain_blocked": "Domain hidden", "account.domain_blocked": "Domain hidden",
@ -39,6 +42,10 @@
"account.unfollow": "Prestani slijediti", "account.unfollow": "Prestani slijediti",
"account.unmute": "Poništi utišavanje @{name}", "account.unmute": "Poništi utišavanje @{name}",
"account.unmute_notifications": "Unmute notifications from @{name}", "account.unmute_notifications": "Unmute notifications from @{name}",
"account_note.cancel": "Cancel",
"account_note.edit": "Edit",
"account_note.placeholder": "No comment provided",
"account_note.save": "Save",
"alert.rate_limited.message": "Please retry after {retry_time, time, medium}.", "alert.rate_limited.message": "Please retry after {retry_time, time, medium}.",
"alert.rate_limited.title": "Rate limited", "alert.rate_limited.title": "Rate limited",
"alert.unexpected.message": "An unexpected error occurred.", "alert.unexpected.message": "An unexpected error occurred.",
@ -236,6 +243,7 @@
"keyboard_shortcuts.reply": "to reply", "keyboard_shortcuts.reply": "to reply",
"keyboard_shortcuts.requests": "to open follow requests list", "keyboard_shortcuts.requests": "to open follow requests list",
"keyboard_shortcuts.search": "to focus search", "keyboard_shortcuts.search": "to focus search",
"keyboard_shortcuts.spoilers": "to show/hide CW field",
"keyboard_shortcuts.start": "to open \"get started\" column", "keyboard_shortcuts.start": "to open \"get started\" column",
"keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", "keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toggle_sensitivity": "to show/hide media", "keyboard_shortcuts.toggle_sensitivity": "to show/hide media",
@ -412,6 +420,10 @@
"time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
"time_remaining.moments": "Moments remaining", "time_remaining.moments": "Moments remaining",
"time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
"timeline_hint.remote_resource_not_displayed": "{resource} from other servers are not displayed.",
"timeline_hint.resources.followers": "Followers",
"timeline_hint.resources.follows": "Follows",
"timeline_hint.resources.statuses": "Older toots",
"trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking", "trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
"trends.trending_now": "Trending now", "trends.trending_now": "Trending now",
"ui.beforeunload": "Your draft will be lost if you leave Mastodon.", "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",

View File

@ -1,10 +1,13 @@
{ {
"account.account_note_header": "Your note for @{name}",
"account.add_account_note": "Add note for @{name}",
"account.add_or_remove_from_list": "Hozzáadás vagy eltávolítás a listáról", "account.add_or_remove_from_list": "Hozzáadás vagy eltávolítás a listáról",
"account.badges.bot": "Bot", "account.badges.bot": "Bot",
"account.badges.group": "Csoport", "account.badges.group": "Csoport",
"account.block": "@{name} letiltása", "account.block": "@{name} letiltása",
"account.block_domain": "Minden elrejtése innen: {domain}", "account.block_domain": "Minden elrejtése innen: {domain}",
"account.blocked": "Letiltva", "account.blocked": "Letiltva",
"account.browse_more_on_origin_server": "További böngészés az eredeti profilon",
"account.cancel_follow_request": "Követési kérelem törlése", "account.cancel_follow_request": "Követési kérelem törlése",
"account.direct": "Közvetlen üzenet @{name} számára", "account.direct": "Közvetlen üzenet @{name} számára",
"account.domain_blocked": "Rejtett domain", "account.domain_blocked": "Rejtett domain",
@ -39,6 +42,10 @@
"account.unfollow": "Követés vége", "account.unfollow": "Követés vége",
"account.unmute": "@{name} némítás feloldása", "account.unmute": "@{name} némítás feloldása",
"account.unmute_notifications": "@{name} némított értesítéseinek feloldása", "account.unmute_notifications": "@{name} némított értesítéseinek feloldása",
"account_note.cancel": "Cancel",
"account_note.edit": "Edit",
"account_note.placeholder": "No comment provided",
"account_note.save": "Save",
"alert.rate_limited.message": "Próbáld újra {retry_time, time, medium} után.", "alert.rate_limited.message": "Próbáld újra {retry_time, time, medium} után.",
"alert.rate_limited.title": "Forgalomkorlátozás", "alert.rate_limited.title": "Forgalomkorlátozás",
"alert.unexpected.message": "Váratlan hiba történt.", "alert.unexpected.message": "Váratlan hiba történt.",
@ -74,9 +81,9 @@
"column_header.show_settings": "Beállítások mutatása", "column_header.show_settings": "Beállítások mutatása",
"column_header.unpin": "Kitűzés eltávolítása", "column_header.unpin": "Kitűzés eltávolítása",
"column_subheading.settings": "Beállítások", "column_subheading.settings": "Beállítások",
"community.column_settings.local_only": "Local only", "community.column_settings.local_only": "Csak helyi",
"community.column_settings.media_only": "Csak média", "community.column_settings.media_only": "Csak média",
"community.column_settings.remote_only": "Remote only", "community.column_settings.remote_only": "Csak távoli",
"compose_form.direct_message_warning": "Ezt a tülköt csak a benne megemlített felhasználók láthatják majd.", "compose_form.direct_message_warning": "Ezt a tülköt csak a benne megemlített felhasználók láthatják majd.",
"compose_form.direct_message_warning_learn_more": "Tudj meg többet", "compose_form.direct_message_warning_learn_more": "Tudj meg többet",
"compose_form.hashtag_warning": "Ez a tülköd nem fog megjelenni semmilyen hashtag alatt mivel listázatlan. Csak nyilvános tülkök kereshetőek hashtaggel.", "compose_form.hashtag_warning": "Ez a tülköd nem fog megjelenni semmilyen hashtag alatt mivel listázatlan. Csak nyilvános tülkök kereshetőek hashtaggel.",
@ -236,6 +243,7 @@
"keyboard_shortcuts.reply": "válasz", "keyboard_shortcuts.reply": "válasz",
"keyboard_shortcuts.requests": "követési kérések listájának megnyitása", "keyboard_shortcuts.requests": "követési kérések listájának megnyitása",
"keyboard_shortcuts.search": "fókuszálás a keresőre", "keyboard_shortcuts.search": "fókuszálás a keresőre",
"keyboard_shortcuts.spoilers": "CW mező mutatása/elrejtése",
"keyboard_shortcuts.start": "\"Első lépések\" megnyitása", "keyboard_shortcuts.start": "\"Első lépések\" megnyitása",
"keyboard_shortcuts.toggle_hidden": "tartalmi figyelmeztetéssel ellátott szöveg mutatása/elrejtése", "keyboard_shortcuts.toggle_hidden": "tartalmi figyelmeztetéssel ellátott szöveg mutatása/elrejtése",
"keyboard_shortcuts.toggle_sensitivity": "média mutatása/elrejtése", "keyboard_shortcuts.toggle_sensitivity": "média mutatása/elrejtése",
@ -412,6 +420,10 @@
"time_remaining.minutes": "{number, plural, one {# perc} other {# perc}} van hátra", "time_remaining.minutes": "{number, plural, one {# perc} other {# perc}} van hátra",
"time_remaining.moments": "Pillanatok vannak hátra", "time_remaining.moments": "Pillanatok vannak hátra",
"time_remaining.seconds": "{number, plural, one {# másodperc} other {# másodperc}} van hátra", "time_remaining.seconds": "{number, plural, one {# másodperc} other {# másodperc}} van hátra",
"timeline_hint.remote_resource_not_displayed": "más szerverekről származó {resource} tartalmakat nem mutatjuk.",
"timeline_hint.resources.followers": "Követő",
"timeline_hint.resources.follows": "Követett",
"timeline_hint.resources.statuses": "Régi tülkök",
"trends.count_by_accounts": "{count} {rawCount, plural, one {résztvevő} other {résztvevő}} beszélget", "trends.count_by_accounts": "{count} {rawCount, plural, one {résztvevő} other {résztvevő}} beszélget",
"trends.trending_now": "Most felkapott", "trends.trending_now": "Most felkapott",
"ui.beforeunload": "A piszkozatod el fog veszni, ha elhagyod a Mastodont.", "ui.beforeunload": "A piszkozatod el fog veszni, ha elhagyod a Mastodont.",

View File

@ -1,10 +1,13 @@
{ {
"account.account_note_header": "Your note for @{name}",
"account.add_account_note": "Add note for @{name}",
"account.add_or_remove_from_list": "Աւելացնել կամ հեռացնել ցանկերից", "account.add_or_remove_from_list": "Աւելացնել կամ հեռացնել ցանկերից",
"account.badges.bot": "Բոտ", "account.badges.bot": "Բոտ",
"account.badges.group": "Խումբ", "account.badges.group": "Խումբ",
"account.block": "Արգելափակել @{name}֊ին", "account.block": "Արգելափակել @{name}֊ին",
"account.block_domain": "Թաքցնել ամէնը հետեւեալ տիրոյթից՝ {domain}", "account.block_domain": "Թաքցնել ամէնը հետեւեալ տիրոյթից՝ {domain}",
"account.blocked": "Արգելափակուած է", "account.blocked": "Արգելափակուած է",
"account.browse_more_on_origin_server": "Browse more on the original profile",
"account.cancel_follow_request": "չեղարկել հետեւելու հայցը", "account.cancel_follow_request": "չեղարկել հետեւելու հայցը",
"account.direct": "Նամակ գրել @{name} -ին", "account.direct": "Նամակ գրել @{name} -ին",
"account.domain_blocked": "Տիրոյթը արգելափակուած է", "account.domain_blocked": "Տիրոյթը արգելափակուած է",
@ -17,7 +20,7 @@
"account.follows.empty": "Այս օգտատէրը դեռ ոչ մէկի չի հետեւում։", "account.follows.empty": "Այս օգտատէրը դեռ ոչ մէկի չի հետեւում։",
"account.follows_you": "Հետեւում է քեզ", "account.follows_you": "Հետեւում է քեզ",
"account.hide_reblogs": "Թաքցնել @{name}֊ի տարածածները", "account.hide_reblogs": "Թաքցնել @{name}֊ի տարածածները",
"account.last_status": "Վերջին անգամ ակտիւ էր", "account.last_status": "Վերջին թութը",
"account.link_verified_on": "Սոյն յղման տիրապետումը ստուգուած է՝ {date}֊ին", "account.link_verified_on": "Սոյն յղման տիրապետումը ստուգուած է՝ {date}֊ին",
"account.locked_info": "Սոյն հաշուի գաղտնիութեան մակարդակը նշուած է որպէս՝ փակ։ Հաշուի տէրն ընտրում է, թէ ով կարող է հետեւել իրեն։", "account.locked_info": "Սոյն հաշուի գաղտնիութեան մակարդակը նշուած է որպէս՝ փակ։ Հաշուի տէրն ընտրում է, թէ ով կարող է հետեւել իրեն։",
"account.media": "Մեդիա", "account.media": "Մեդիա",
@ -39,6 +42,10 @@
"account.unfollow": "Ապահետեւել", "account.unfollow": "Ապահետեւել",
"account.unmute": "Ապալռեցնել @{name}֊ին", "account.unmute": "Ապալռեցնել @{name}֊ին",
"account.unmute_notifications": "Միացնել ծանուցումները @{name}֊ից", "account.unmute_notifications": "Միացնել ծանուցումները @{name}֊ից",
"account_note.cancel": "Cancel",
"account_note.edit": "Edit",
"account_note.placeholder": "No comment provided",
"account_note.save": "Save",
"alert.rate_limited.message": "Փորձէք որոշ ժամանակ անց՝ {retry_time, time, medium}։", "alert.rate_limited.message": "Փորձէք որոշ ժամանակ անց՝ {retry_time, time, medium}։",
"alert.rate_limited.title": "Գործողութիւնների յաճախութիւնը գերազանցում է թոյլատրելին", "alert.rate_limited.title": "Գործողութիւնների յաճախութիւնը գերազանցում է թոյլատրելին",
"alert.unexpected.message": "Անսպասելի սխալ տեղի ունեցաւ։", "alert.unexpected.message": "Անսպասելի սխալ տեղի ունեցաւ։",
@ -58,7 +65,7 @@
"column.direct": "Հասցէագրուած հաղորդագրութիւններ", "column.direct": "Հասցէագրուած հաղորդագրութիւններ",
"column.directory": "Զննել անձնական էջերը", "column.directory": "Զննել անձնական էջերը",
"column.domain_blocks": "Թաքցուած տիրոյթները", "column.domain_blocks": "Թաքցուած տիրոյթները",
"column.favourites": "Հավանածներ", "column.favourites": "Հաւանածներ",
"column.follow_requests": "Հետեւելու հայցեր", "column.follow_requests": "Հետեւելու հայցեր",
"column.home": "Հիմնական", "column.home": "Հիմնական",
"column.lists": "Ցանկեր", "column.lists": "Ցանկեր",
@ -68,27 +75,27 @@
"column.public": "Դաշնային հոսք", "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": "Տեղաշարժել սիւնը ձախ",
"column_header.moveRight_settings": "Տեղաշարժել սյունը աջ", "column_header.moveRight_settings": "Տեղաշարժել սիւնը աջ",
"column_header.pin": "Ամրացնել", "column_header.pin": "Ամրացնել",
"column_header.show_settings": "Ցուցադրել կարգավորումները", "column_header.show_settings": "Ցուցադրել կարգավորումները",
"column_header.unpin": "Հանել", "column_header.unpin": "Հանել",
"column_subheading.settings": "Կարգավորումներ", "column_subheading.settings": "Կարգավորումներ",
"community.column_settings.local_only": "Local only", "community.column_settings.local_only": "Միայն ներքին",
"community.column_settings.media_only": "Media only", "community.column_settings.media_only": "Media only",
"community.column_settings.remote_only": "Remote only", "community.column_settings.remote_only": "Միայն հեռակա",
"compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.", "compose_form.direct_message_warning": "This toot will only be visible to all the mentioned users.",
"compose_form.direct_message_warning_learn_more": "Իմանալ ավելին", "compose_form.direct_message_warning_learn_more": "Իմանալ ավելին",
"compose_form.hashtag_warning": "Այս թութը չի հաշվառվի որեւէ պիտակի տակ, քանզի այն ծածուկ է։ Միայն հրապարակային թթերը հնարավոր է որոնել պիտակներով։", "compose_form.hashtag_warning": "Այս թութը չի հաշվառվի որեւէ պիտակի տակ, քանզի այն ծածուկ է։ Միայն հրապարակային թթերը հնարավոր է որոնել պիտակներով։",
"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.poll.add_option": "Աւելացնել տարբերակ", "compose_form.poll.add_option": "Աւելացնել տարբերակ",
"compose_form.poll.duration": "Հարցման տեւողութիւնը", "compose_form.poll.duration": "Հարցման տեւողութիւնը",
"compose_form.poll.option_placeholder": "Տարբերակ {number}", "compose_form.poll.option_placeholder": "Տարբերակ {number}",
"compose_form.poll.remove_option": "Հեռացնել այս տարբերակը", "compose_form.poll.remove_option": "Հեռացնել այս տարբերակը",
"compose_form.poll.switch_to_multiple": "Հարցումը դարձնել բազմակի ընտրությամբ", "compose_form.poll.switch_to_multiple": "Հարցումը դարձնել բազմակի ընտրութեամբ",
"compose_form.poll.switch_to_single": "Հարցումը դարձնել եզակի ընտրությամբ", "compose_form.poll.switch_to_single": "Հարցումը դարձնել եզակի ընտրութեամբ",
"compose_form.publish": "Թթել", "compose_form.publish": "Թթել",
"compose_form.publish_loud": "Թթե՜լ", "compose_form.publish_loud": "Թթե՜լ",
"compose_form.sensitive.hide": "Նշել մեդիան որպէս դիւրազգաց", "compose_form.sensitive.hide": "Նշել մեդիան որպէս դիւրազգաց",
@ -124,24 +131,24 @@
"conversation.with": "{names}֊երի հետ", "conversation.with": "{names}֊երի հետ",
"directory.federated": "Յայտնի դաշնեզերքից", "directory.federated": "Յայտնի դաշնեզերքից",
"directory.local": "{domain} տիրոյթից միայն", "directory.local": "{domain} տիրոյթից միայն",
"directory.new_arrivals": "Նորութիւններ", "directory.new_arrivals": "Նորեկներ",
"directory.recently_active": "Վերջերս ակտիւ", "directory.recently_active": "Վերջերս ակտիւ",
"embed.instructions": "Այս թութը քո կայքում ներդնելու համար կարող ես պատճենել ներքոհիշյալ կոդը։", "embed.instructions": "Այս թութը քո կայքում ներդնելու համար կարող ես պատճէնել ներքինանալ կոդը։",
"embed.preview": "Ահա, թե ինչ տեսք կունենա այն՝", "embed.preview": "Ահա, թե ինչ տեսք կունենա այն՝",
"emoji_button.activity": "Զբաղմունքներ", "emoji_button.activity": "Զբաղմունքներ",
"emoji_button.custom": "Հատուկ", "emoji_button.custom": "Հատուկ",
"emoji_button.flags": "Դրոշներ", "emoji_button.flags": "Դրոշներ",
"emoji_button.food": "Կերուխում", "emoji_button.food": "Կերուխում",
"emoji_button.label": "Էմոջի ավելացնել", "emoji_button.label": "Էմոջի ավելացնել",
"emoji_button.nature": "Բնություն", "emoji_button.nature": "Բնութիւն",
"emoji_button.not_found": "Նման էմոջիներ դեռ չեն հայտնաբերվել։ (╯°□°)╯︵ ┻━┻", "emoji_button.not_found": "Նման էմոջիներ դեռ չեն հայտնաբերվել։ (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "Առարկաներ", "emoji_button.objects": "Առարկաներ",
"emoji_button.people": "Մարդիկ", "emoji_button.people": "Մարդիկ",
"emoji_button.recent": "Հաճախ օգտագործվող", "emoji_button.recent": "Հաճախ օգտագործվող",
"emoji_button.search": "Որոնել…", "emoji_button.search": "Որոնել…",
"emoji_button.search_results": "Որոնման արդյունքներ", "emoji_button.search_results": "Որոնման արդիւնքներ",
"emoji_button.symbols": "Նշաններ", "emoji_button.symbols": "Նշաններ",
"emoji_button.travel": "Ուղեւորություն եւ տեղանքներ", "emoji_button.travel": "Ուղեւորութիւն եւ տեղանքներ",
"empty_column.account_timeline": "Այստեղ թթեր չկա՛ն։", "empty_column.account_timeline": "Այստեղ թթեր չկա՛ն։",
"empty_column.account_unavailable": "Անձնական էջը հասանելի չի", "empty_column.account_unavailable": "Անձնական էջը հասանելի չի",
"empty_column.blocks": "Դու դեռ ոչ մէկի չես արգելափակել։", "empty_column.blocks": "Դու դեռ ոչ մէկի չես արգելափակել։",
@ -151,31 +158,31 @@
"empty_column.domain_blocks": "Թաքցուած տիրոյթներ դեռ չկան։", "empty_column.domain_blocks": "Թաքցուած տիրոյթներ դեռ չկան։",
"empty_column.favourited_statuses": "Դու դեռ չունես որեւէ հաւանած թութ։ Երբ հաւանես, դրանք կերեւան այստեղ։", "empty_column.favourited_statuses": "Դու դեռ չունես որեւէ հաւանած թութ։ Երբ հաւանես, դրանք կերեւան այստեղ։",
"empty_column.favourites": "Այս թութը ոչ մէկ դեռ չի հաւանել։ Հաւանողները կերեւան այստեղ, երբ նշեն թութը հաւանած։", "empty_column.favourites": "Այս թութը ոչ մէկ դեռ չի հաւանել։ Հաւանողները կերեւան այստեղ, երբ նշեն թութը հաւանած։",
"empty_column.follow_requests": "Դու դեռ չունես որեւէ հետևելու հայտ։ Բոլոր նման հայտերը կհայտնվեն այստեղ։", "empty_column.follow_requests": "Դու դեռ չունես որեւէ հետեւելու յայտ։ Բոլոր նման յայտերը կը յայտնուեն այստեղ։",
"empty_column.hashtag": "Այս պիտակով դեռ ոչինչ չկա։", "empty_column.hashtag": "Այս պիտակով դեռ ոչինչ չկա։",
"empty_column.home": "Քո հիմնական հոսքը դատա՛րկ է։ Այցելի՛ր {public}ը կամ օգտվիր որոնումից՝ այլ մարդկանց հանդիպելու համար։", "empty_column.home": "Քո հիմնական հոսքը դատա՛րկ է։ Այցելի՛ր {public}ը կամ օգտվիր որոնումից՝ այլ մարդկանց հանդիպելու համար։",
"empty_column.home.public_timeline": "հրապարակային հոսք", "empty_column.home.public_timeline": "հրապարակային հոսք",
"empty_column.list": "Այս ցանկում դեռ ոչինչ չկա։ Երբ ցանկի անդամներից որեւէ մեկը նոր թութ գրի, այն կհայտնվի այստեղ։", "empty_column.list": "Այս ցանկում դեռ ոչինչ չկա։ Երբ ցանկի անդամներից որեւէ մեկը նոր թութ գրի, այն կհայտնվի այստեղ։",
"empty_column.lists": "Դուք դեռ չունեք ստեղծած ցանկ։ Ցանկ ստեղծելուն պես այն կհայտնվի այստեղ։", "empty_column.lists": "Դուք դեռ չունեք ստեղծած ցանկ։ Ցանկ ստեղծելուն պես այն կհայտնվի այստեղ։",
"empty_column.mutes": "Առայժմ ոչ ոքի չեք լռեցրել։", "empty_column.mutes": "Առայժմ ոչ ոքի չեք լռեցրել։",
"empty_column.notifications": "Ոչ մի ծանուցում դեռ չունես։ Բզիր մյուսներին՝ խոսակցությունը սկսելու համար։", "empty_column.notifications": "Ոչ մի ծանուցում դեռ չունես։ Բզիր միւսներին՝ խօսակցութիւնը սկսելու համար։",
"empty_column.public": "Այստեղ բան չկա՛։ Հրապարակային մի բան գրիր կամ հետեւիր այլ հանգույցներից էակների՝ այն լցնելու համար։", "empty_column.public": "Այստեղ բան չկա՛յ։ Հրապարակային մի բան գրիր կամ հետեւիր այլ հանգոյցներից էակների՝ այն լցնելու համար։",
"error.unexpected_crash.explanation": "Մեր ծրագրակազմում վրիպակի կամ դիտարկչի անհամատեղելիության պատճառով այս էջը չի կարող լիարժեք պատկերվել։", "error.unexpected_crash.explanation": "Մեր ծրագրակազմում վրիպակի կամ դիտարկչի անհամատեղելիութեան պատճառով այս էջը չի կարող լիարժէք պատկերուել։",
"error.unexpected_crash.next_steps": "Փորձիր թարմացնել էջը։ Եթե դա չօգնի ապա կարող ես օգտվել Մաստադոնից ուրիշ դիտարկիչով կամ հավելվածով։", "error.unexpected_crash.next_steps": "Փորձիր թարմացնել էջը։ Եթե դա չօգնի ապա կարող ես օգտվել Մաստադոնից ուրիշ դիտարկիչով կամ հավելվածով։",
"errors.unexpected_crash.copy_stacktrace": "Պատճենել սթաքթրեյսը սեղմատախտակին", "errors.unexpected_crash.copy_stacktrace": "Պատճենել սթաքթրեյսը սեղմատախտակին",
"errors.unexpected_crash.report_issue": "Զեկուցել խնդրի մասին", "errors.unexpected_crash.report_issue": "Զեկուցել խնդրի մասին",
"follow_request.authorize": "Վավերացնել", "follow_request.authorize": "Վավերացնել",
"follow_request.reject": "Մերժել", "follow_request.reject": "Մերժել",
"follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.", "follow_requests.unlocked_explanation": "Այս հարցումը ուղարկված է հաշվից, որի համար {domain}-ի անձնակազմը միացրել է ձեռքով ստուգում։",
"getting_started.developers": "Մշակողներ", "getting_started.developers": "Մշակողներ",
"getting_started.directory": "Օգտատերերի շտեմարան", "getting_started.directory": "Օգտատէրերի շտեմարան",
"getting_started.documentation": "Փաստաթղթեր", "getting_started.documentation": "Փաստաթղթեր",
"getting_started.heading": "Ինչպես սկսել", "getting_started.heading": "Ինչպես սկսել",
"getting_started.invite": "Հրավիրել մարդկանց", "getting_started.invite": "Հրաւիրել մարդկանց",
"getting_started.open_source_notice": "Մաստոդոնը բաց ելատեքստով ծրագրակազմ է։ Կարող ես ներդրում անել կամ վրեպներ զեկուցել ԳիթՀաբում՝ {github}։", "getting_started.open_source_notice": "Մաստոդոնը բաց ելատեքստով ծրագրակազմ է։ Կարող ես ներդրում անել կամ վրեպներ զեկուցել ԳիթՀաբում՝ {github}։",
"getting_started.security": "Հաշվի կարգավորումներ", "getting_started.security": "Հաշուի կարգաւորումներ",
"getting_started.terms": "Ծառայության պայմանները", "getting_started.terms": "Ծառայութեան պայմանները",
"hashtag.column_header.tag_mode.all": "և {additional}", "hashtag.column_header.tag_mode.all": "եւ {additional}",
"hashtag.column_header.tag_mode.any": "կամ {additional}", "hashtag.column_header.tag_mode.any": "կամ {additional}",
"hashtag.column_header.tag_mode.none": "առանց {additional}", "hashtag.column_header.tag_mode.none": "առանց {additional}",
"hashtag.column_settings.select.no_options_message": "Առաջարկներ չկան", "hashtag.column_settings.select.no_options_message": "Առաջարկներ չկան",
@ -187,34 +194,34 @@
"home.column_settings.basic": "Հիմնական", "home.column_settings.basic": "Հիմնական",
"home.column_settings.show_reblogs": "Ցուցադրել տարածածները", "home.column_settings.show_reblogs": "Ցուցադրել տարածածները",
"home.column_settings.show_replies": "Ցուցադրել պատասխանները", "home.column_settings.show_replies": "Ցուցադրել պատասխանները",
"home.hide_announcements": "Թաքցնել հայտարարությունները", "home.hide_announcements": "Թաքցնել յայտարարութիւնները",
"home.show_announcements": "Ցուցադրել հայտարարությունները", "home.show_announcements": "Ցուցադրել յայտարարութիւնները",
"intervals.full.days": "{number, plural, one {# օր} other {# օր}}", "intervals.full.days": "{number, plural, one {# օր} other {# օր}}",
"intervals.full.hours": "{number, plural, one {# ժամ} other {# ժամ}}", "intervals.full.hours": "{number, plural, one {# ժամ} other {# ժամ}}",
"intervals.full.minutes": "{number, plural, one {# րոպե} other {# րոպե}}", "intervals.full.minutes": "{number, plural, one {# րոպե} other {# րոպե}}",
"introduction.federation.action": "Հաջորդ", "introduction.federation.action": "Հաջորդ",
"introduction.federation.federated.headline": "Դաշնային", "introduction.federation.federated.headline": "Դաշնային",
"introduction.federation.federated.text": "Դաշտնեզերքի հարևան հանգույցների հանրային գրառումները կհայտնվեն դաշնային հոսքում։", "introduction.federation.federated.text": "Դաշնեզերքի հարեւան հանգոյցների հանրային գրառումները կը յայտնուեն դաշնային հոսքում։",
"introduction.federation.home.headline": "Հիմնական", "introduction.federation.home.headline": "Հիմնական",
"introduction.federation.home.text": "Այն անձանց թթերը ում հետևում ես, կհայտնվի հիմնական հոսքում։ Դու կարող ես հետևել ցանկացած անձի ցանկացած հանգույցից։", "introduction.federation.home.text": "Այն անձանց թթերը ում հետևում ես, կը յայտնուեն հիմնական հոսքում։ Դու կարող ես հետեւել ցանկացած անձի ցանկացած հանգոյցից։",
"introduction.federation.local.headline": "Տեղային", "introduction.federation.local.headline": "Տեղային",
"introduction.federation.local.text": "Տեղական հոսքում կարող ես տեսնել քո հանգույցի բոլոր հանրային գրառումները։", "introduction.federation.local.text": "Տեղական հոսքում կարող ես տեսնել քո հանգոյցի բոլոր հանրային գրառումները։",
"introduction.interactions.action": "Finish toot-orial!", "introduction.interactions.action": "Finish toot-orial!",
"introduction.interactions.favourite.headline": "Նախընտրելի", "introduction.interactions.favourite.headline": "Նախընտրելի",
"introduction.interactions.favourite.text": "Փոխանցիր հեղինակին որ քեզ դուր է եկել իր թութը հավանելով այն։", "introduction.interactions.favourite.text": "Փոխանցիր հեղինակին որ քեզ դուր է եկել իր թութը հավանելով այն։",
"introduction.interactions.reblog.headline": "Տարածել", "introduction.interactions.reblog.headline": "Տարածել",
"introduction.interactions.reblog.text": "Կիսիր այլ օգտատերերի թութերը քո հետևորդների հետ տարածելով դրանք քո անձնական էջում։", "introduction.interactions.reblog.text": "Կիսիր այլ օգտատէրերի թութերը քո հետեւողների հետ տարածելով դրանք քո անձնական էջում։",
"introduction.interactions.reply.headline": "Պատասխանել", "introduction.interactions.reply.headline": "Պատասխանել",
"introduction.interactions.reply.text": "Արձագանքիր ուրիշների և քո թթերին, դրանք կդարսվեն մեկ ընհանուր քննարկման շղթայով։", "introduction.interactions.reply.text": "Արձագանքիր ուրիշների եւ քո թթերին, դրանք կը դարսուեն մէկ ընդհանուր քննարկման շղթայով։",
"introduction.welcome.action": "Գնացի՜նք։", "introduction.welcome.action": "Գնացի՜նք։",
"introduction.welcome.headline": "Առաջին քայլեր", "introduction.welcome.headline": "Առաջին քայլեր",
"introduction.welcome.text": "Դաշնեզերքը ողջունում է ձեզ։ Շուտով կկարողանաս ուղարկել նամակներ ու շփվել տարբեր հանգույցների ընկերներիդ հետ։ Բայց մտապահիր {domain} հանգույցը, այն յուրահատուկ է, այստեղ է պահվում քո հաշիվը։", "introduction.welcome.text": "Դաշնեզերքը ողջունում է ձեզ։ Շուտով կը կարողանաս ուղարկել նամակներ ու շփուել տարբեր հանգոյցների ընկերներիդ հետ։ Բայց մտապահիր {domain} հանգոյցը, այն իւրայատուկ է, այստեղ է պահւում քո հաշիւը։",
"keyboard_shortcuts.back": "ետ նավարկելու համար", "keyboard_shortcuts.back": "ետ նավարկելու համար",
"keyboard_shortcuts.blocked": "արգելափակված օգտատերերի ցանկը բացելու համար", "keyboard_shortcuts.blocked": "արգելափակված օգտատերերի ցանկը բացելու համար",
"keyboard_shortcuts.boost": "տարածելու համար", "keyboard_shortcuts.boost": "տարածելու համար",
"keyboard_shortcuts.column": "սյուներից մեկի վրա սեւեռվելու համար", "keyboard_shortcuts.column": "սիւներից մէկի վրայ սեւեռուելու համար",
"keyboard_shortcuts.compose": "շարադրման տիրույթին սեւեռվելու համար", "keyboard_shortcuts.compose": "շարադրման տիրույթին սեւեռվելու համար",
"keyboard_shortcuts.description": "Նկարագրություն", "keyboard_shortcuts.description": "Նկարագրութիւն",
"keyboard_shortcuts.direct": "հասցեագրված գրվածքների հոսքը բացելու համար", "keyboard_shortcuts.direct": "հասցեագրված գրվածքների հոսքը բացելու համար",
"keyboard_shortcuts.down": "ցանկով ներքեւ շարժվելու համար", "keyboard_shortcuts.down": "ցանկով ներքեւ շարժվելու համար",
"keyboard_shortcuts.enter": "թութը բացելու համար", "keyboard_shortcuts.enter": "թութը բացելու համար",
@ -229,13 +236,14 @@
"keyboard_shortcuts.mention": "հեղինակին նշելու համար", "keyboard_shortcuts.mention": "հեղինակին նշելու համար",
"keyboard_shortcuts.muted": "լռեցված օգտատերերի ցանկը բացելու համար", "keyboard_shortcuts.muted": "լռեցված օգտատերերի ցանկը բացելու համար",
"keyboard_shortcuts.my_profile": "սեփական էջին անցնելու համար", "keyboard_shortcuts.my_profile": "սեփական էջին անցնելու համար",
"keyboard_shortcuts.notifications": "ծանուցումեների սյունակը բացելու համար", "keyboard_shortcuts.notifications": "ծանուցումների սիւնակը բացելու համար",
"keyboard_shortcuts.open_media": "ցուցադրել մեդիան", "keyboard_shortcuts.open_media": "ցուցադրել մեդիան",
"keyboard_shortcuts.pinned": "ամրացուած թթերի ցանկը բացելու համար", "keyboard_shortcuts.pinned": "ամրացուած թթերի ցանկը բացելու համար",
"keyboard_shortcuts.profile": "հեղինակի անձնական էջը բացելու համար", "keyboard_shortcuts.profile": "հեղինակի անձնական էջը բացելու համար",
"keyboard_shortcuts.reply": "պատասխանելու համար", "keyboard_shortcuts.reply": "պատասխանելու համար",
"keyboard_shortcuts.requests": "հետեւելու հայցերի ցանկը դիտելու համար", "keyboard_shortcuts.requests": "հետեւելու հայցերի ցանկը դիտելու համար",
"keyboard_shortcuts.search": "որոնման դաշտին սեւեռվելու համար", "keyboard_shortcuts.search": "որոնման դաշտին սեւեռվելու համար",
"keyboard_shortcuts.spoilers": "to show/hide CW field",
"keyboard_shortcuts.start": "«սկսել» սիւնակը բացելու համար", "keyboard_shortcuts.start": "«սկսել» սիւնակը բացելու համար",
"keyboard_shortcuts.toggle_hidden": "CW֊ի ետեւի տեքստը ցուցադրել֊թաքցնելու համար", "keyboard_shortcuts.toggle_hidden": "CW֊ի ետեւի տեքստը ցուցադրել֊թաքցնելու համար",
"keyboard_shortcuts.toggle_sensitivity": "մեդիան ցուցադրել֊թաքցնելու համար", "keyboard_shortcuts.toggle_sensitivity": "մեդիան ցուցադրել֊թաքցնելու համար",
@ -255,7 +263,7 @@
"lists.new.title_placeholder": "Նոր ցանկի վերնագիր", "lists.new.title_placeholder": "Նոր ցանկի վերնագիր",
"lists.search": "Փնտրել քո հետեւած մարդկանց մեջ", "lists.search": "Փնտրել քո հետեւած մարդկանց մեջ",
"lists.subheading": "Քո ցանկերը", "lists.subheading": "Քո ցանկերը",
"load_pending": "{count, plural, one {# նոր նյութ} other {# նոր նյութ}}", "load_pending": "{count, plural, one {# նոր նիւթ} other {# նոր նիւթ}}",
"loading_indicator.label": "Բեռնվում է…", "loading_indicator.label": "Բեռնվում է…",
"media_gallery.toggle_visible": "Ցուցադրել/թաքցնել", "media_gallery.toggle_visible": "Ցուցադրել/թաքցնել",
"missing_indicator.label": "Չգտնվեց", "missing_indicator.label": "Չգտնվեց",
@ -270,23 +278,23 @@
"navigation_bar.discover": "Բացայայտել", "navigation_bar.discover": "Բացայայտել",
"navigation_bar.domain_blocks": "Թաքցուած տիրոյթներ", "navigation_bar.domain_blocks": "Թաքցուած տիրոյթներ",
"navigation_bar.edit_profile": "Խմբագրել անձնական էջը", "navigation_bar.edit_profile": "Խմբագրել անձնական էջը",
"navigation_bar.favourites": "Հավանածներ", "navigation_bar.favourites": "Հաւանածներ",
"navigation_bar.filters": "Լռեցուած բառեր", "navigation_bar.filters": "Լռեցուած բառեր",
"navigation_bar.follow_requests": "Հետեւելու հայցեր", "navigation_bar.follow_requests": "Հետեւելու հայցեր",
"navigation_bar.follows_and_followers": "Հետեւածներ եւ հետեւողներ", "navigation_bar.follows_and_followers": "Հետեւածներ եւ հետեւողներ",
"navigation_bar.info": "Այս հանգույցի մասին", "navigation_bar.info": "Այս հանգոյցի մասին",
"navigation_bar.keyboard_shortcuts": "Ստեղնաշարի կարճատներ", "navigation_bar.keyboard_shortcuts": "Ստեղնաշարի կարճատներ",
"navigation_bar.lists": "Ցանկեր", "navigation_bar.lists": "Ցանկեր",
"navigation_bar.logout": "Դուրս գալ", "navigation_bar.logout": "Դուրս գալ",
"navigation_bar.mutes": "Լռեցրած օգտատերեր", "navigation_bar.mutes": "Լռեցրած օգտատերեր",
"navigation_bar.personal": "Անձնական", "navigation_bar.personal": "Անձնական",
"navigation_bar.pins": "Ամրացված թթեր", "navigation_bar.pins": "Ամրացված թթեր",
"navigation_bar.preferences": "Նախապատվություններ", "navigation_bar.preferences": "Նախապատուութիւններ",
"navigation_bar.public_timeline": "Դաշնային հոսք", "navigation_bar.public_timeline": "Դաշնային հոսք",
"navigation_bar.security": "Անվտանգություն", "navigation_bar.security": "Անվտանգութիւն",
"notification.favourite": "{name} հավանեց թութդ", "notification.favourite": "{name} հավանեց թութդ",
"notification.follow": "{name} սկսեց հետեւել քեզ", "notification.follow": "{name} սկսեց հետեւել քեզ",
"notification.follow_request": "{name} քեզ հետևելու հայց է ուղարկել", "notification.follow_request": "{name} քեզ հետեւելու հայց է ուղարկել",
"notification.mention": "{name} նշեց քեզ", "notification.mention": "{name} նշեց քեզ",
"notification.own_poll": "Հարցումդ աւարտուեց", "notification.own_poll": "Հարցումդ աւարտուեց",
"notification.poll": "Հարցումը, ուր դու քուէարկել ես, աւարտուեց։", "notification.poll": "Հարցումը, ուր դու քուէարկել ես, աւարտուեց։",
@ -294,7 +302,7 @@
"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.filter_bar.advanced": "Ցուցադրել բոլոր կատեգորիաները", "notifications.column_settings.filter_bar.advanced": "Ցուցադրել բոլոր կատեգորիաները",
"notifications.column_settings.filter_bar.category": "Արագ զտման վահանակ", "notifications.column_settings.filter_bar.category": "Արագ զտման վահանակ",
"notifications.column_settings.filter_bar.show": "Ցուցադրել", "notifications.column_settings.filter_bar.show": "Ցուցադրել",
@ -304,7 +312,7 @@
"notifications.column_settings.poll": "Հարցման արդիւնքները՝", "notifications.column_settings.poll": "Հարցման արդիւնքները՝",
"notifications.column_settings.push": "Հրելու ծանուցումներ", "notifications.column_settings.push": "Հրելու ծանուցումներ",
"notifications.column_settings.reblog": "Տարածածներից՝", "notifications.column_settings.reblog": "Տարածածներից՝",
"notifications.column_settings.show": "Ցուցադրել սյունում", "notifications.column_settings.show": "Ցուցադրել սիւնում",
"notifications.column_settings.sound": "Ձայն հանել", "notifications.column_settings.sound": "Ձայն հանել",
"notifications.filter.all": "Բոլորը", "notifications.filter.all": "Բոլորը",
"notifications.filter.boosts": "Տարածածները", "notifications.filter.boosts": "Տարածածները",
@ -321,7 +329,7 @@
"poll.voted": "Դու քուէարկել ես այս տարբերակի համար", "poll.voted": "Դու քուէարկել ես այս տարբերակի համար",
"poll_button.add_poll": "Աւելացնել հարցում", "poll_button.add_poll": "Աւելացնել հարցում",
"poll_button.remove_poll": "Հեռացնել հարցումը", "poll_button.remove_poll": "Հեռացնել հարցումը",
"privacy.change": "Կարգավորել թթի գաղտնիությունը", "privacy.change": "Կարգաւորել թթի գաղտնիութիւնը",
"privacy.direct.long": "Թթել միայն նշված օգտատերերի համար", "privacy.direct.long": "Թթել միայն նշված օգտատերերի համար",
"privacy.direct.short": "Հասցեագրված", "privacy.direct.short": "Հասցեագրված",
"privacy.private.long": "Թթել միայն հետեւողների համար", "privacy.private.long": "Թթել միայն հետեւողների համար",
@ -343,7 +351,7 @@
"report.forward": "Փոխանցել {target}֊ին", "report.forward": "Փոխանցել {target}֊ին",
"report.forward_hint": "Այս հաշիւ այլ հանգոյցից է։ Ուղարկե՞մ այնտեղ էլ այս բողոքի անոնիմ պատճէնը։", "report.forward_hint": "Այս հաշիւ այլ հանգոյցից է։ Ուղարկե՞մ այնտեղ էլ այս բողոքի անոնիմ պատճէնը։",
"report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:", "report.hint": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
"report.placeholder": "Լրացուցիչ մեկնաբանություններ", "report.placeholder": "Լրացուցիչ մեկնաբանութիւններ",
"report.submit": "Ուղարկել", "report.submit": "Ուղարկել",
"report.target": "Բողոքել {target}֊ի մասին", "report.target": "Բողոքել {target}֊ի մասին",
"search.placeholder": "Փնտրել", "search.placeholder": "Փնտրել",
@ -357,7 +365,7 @@
"search_results.hashtags": "Պիտակներ", "search_results.hashtags": "Պիտակներ",
"search_results.statuses": "Թթեր", "search_results.statuses": "Թթեր",
"search_results.statuses_fts_disabled": "Այս հանգոյցում միացուած չէ ըստ բովանդակութեան թթեր փնտրելու հնարաւորութիւնը։", "search_results.statuses_fts_disabled": "Այս հանգոյցում միացուած չէ ըստ բովանդակութեան թթեր փնտրելու հնարաւորութիւնը։",
"search_results.total": "{count, number} {count, plural, one {արդյունք} other {արդյունք}}", "search_results.total": "{count, number} {count, plural, one {արդիւնք} other {արդիւնք}}",
"status.admin_account": "Բացել @{name} օգտատիրոջ մոդերացիայի դիմերէսը։", "status.admin_account": "Բացել @{name} օգտատիրոջ մոդերացիայի դիմերէսը։",
"status.admin_status": "Բացել այս գրառումը մոդերատորի դիմերէսի մէջ", "status.admin_status": "Բացել այս գրառումը մոդերատորի դիմերէսի մէջ",
"status.block": "Արգելափակել @{name}֊ին", "status.block": "Արգելափակել @{name}֊ին",
@ -372,11 +380,11 @@
"status.favourite": "Հավանել", "status.favourite": "Հավանել",
"status.filtered": "Զտված", "status.filtered": "Զտված",
"status.load_more": "Բեռնել ավելին", "status.load_more": "Բեռնել ավելին",
"status.media_hidden": "մեդիաբովանդակությունը թաքցված է", "status.media_hidden": "մեդիաբովանդակութիւնը թաքցուած է",
"status.mention": "Նշել @{name}֊ին", "status.mention": "Նշել @{name}֊ին",
"status.more": "Ավելին", "status.more": "Ավելին",
"status.mute": "Լռեցնել @{name}֊ին", "status.mute": "Լռեցնել @{name}֊ին",
"status.mute_conversation": "Լռեցնել խոսակցությունը", "status.mute_conversation": "Լռեցնել խօսակցութիւնը",
"status.open": "Ընդարձակել այս թութը", "status.open": "Ընդարձակել այս թութը",
"status.pin": "Ամրացնել անձնական էջում", "status.pin": "Ամրացնել անձնական էջում",
"status.pinned": "Ամրացված թութ", "status.pinned": "Ամրացված թութ",
@ -384,13 +392,13 @@
"status.reblog": "Տարածել", "status.reblog": "Տարածել",
"status.reblog_private": "Տարածել սեփական լսարանին", "status.reblog_private": "Տարածել սեփական լսարանին",
"status.reblogged_by": "{name} տարածել է", "status.reblogged_by": "{name} տարածել է",
"status.reblogs.empty": "Այս թութը ոչ մէկ դեռ չի տարածել։ Տարածողները կերեւան այստեղ, երբ որևէ մեկը տարածի։", "status.reblogs.empty": "Այս թութը ոչ մէկ դեռ չի տարածել։ Տարածողները կերեւան այստեղ, երբ որեւէ մէկը տարածի։",
"status.redraft": "Ջնջել եւ վերակազմել", "status.redraft": "Ջնջել եւ վերակազմել",
"status.remove_bookmark": "Հեռացնել էջանիշերից", "status.remove_bookmark": "Հեռացնել էջանիշերից",
"status.reply": "Պատասխանել", "status.reply": "Պատասխանել",
"status.replyAll": "Պատասխանել թելին", "status.replyAll": "Պատասխանել թելին",
"status.report": "Բողոքել @{name}֊ից", "status.report": "Բողոքել @{name}֊ից",
"status.sensitive_warning": "Կասկածելի բովանդակություն", "status.sensitive_warning": "Կասկածելի բովանդակութիւն",
"status.share": "Կիսվել", "status.share": "Կիսվել",
"status.show_less": "Պակաս", "status.show_less": "Պակաս",
"status.show_less_all": "Թաքցնել բոլոր նախազգուշացնումները", "status.show_less_all": "Թաքցնել բոլոր նախազգուշացնումները",
@ -398,7 +406,7 @@
"status.show_more_all": "Ցուցադրել բոլոր նախազգուշացնումները", "status.show_more_all": "Ցուցադրել բոլոր նախազգուշացնումները",
"status.show_thread": "Բացել շղթան", "status.show_thread": "Բացել շղթան",
"status.uncached_media_warning": "Անհասանելի", "status.uncached_media_warning": "Անհասանելի",
"status.unmute_conversation": "Ապալռեցնել խոսակցությունը", "status.unmute_conversation": "Ապալռեցնել խօսակցութիւնը",
"status.unpin": "Հանել անձնական էջից", "status.unpin": "Հանել անձնական էջից",
"suggestions.dismiss": "Անտեսել առաջարկը", "suggestions.dismiss": "Անտեսել առաջարկը",
"suggestions.header": "Միգուցե քեզ հետաքրքրի…", "suggestions.header": "Միգուցե քեզ հետաքրքրի…",
@ -410,34 +418,38 @@
"time_remaining.days": "{number, plural, one {մնաց # օր} other {մնաց # օր}}", "time_remaining.days": "{number, plural, one {մնաց # օր} other {մնաց # օր}}",
"time_remaining.hours": "{number, plural, one {# ժամ} other {# ժամ}} անց", "time_remaining.hours": "{number, plural, one {# ժամ} other {# ժամ}} անց",
"time_remaining.minutes": "{number, plural, one {# րոպե} other {# րոպե}} անց", "time_remaining.minutes": "{number, plural, one {# րոպե} other {# րոպե}} անց",
"time_remaining.moments": "Մնացել է մի քանի վարկյան", "time_remaining.moments": "Մնացել է մի քանի վարկեան",
"time_remaining.seconds": "{number, plural, one {# վայրկյան} other {# վայրկյան}} անց", "time_remaining.seconds": "{number, plural, one {# վարկեան} other {# վարկեան}} անց",
"timeline_hint.remote_resource_not_displayed": "{resource} from other servers are not displayed.",
"timeline_hint.resources.followers": "Followers",
"timeline_hint.resources.follows": "Follows",
"timeline_hint.resources.statuses": "Older toots",
"trends.count_by_accounts": "{count} {rawCount, plural, one {հոգի} other {հոգի}} խոսում է սրա մասին", "trends.count_by_accounts": "{count} {rawCount, plural, one {հոգի} other {հոգի}} խոսում է սրա մասին",
"trends.trending_now": "Այժմ արդիական", "trends.trending_now": "Այժմ արդիական",
"ui.beforeunload": "Քո սեւագիրը կկորի, եթե լքես Մաստոդոնը։", "ui.beforeunload": "Քո սեւագիրը կկորի, եթե լքես Մաստոդոնը։",
"upload_area.title": "Քաշիր ու նետիր՝ վերբեռնելու համար", "upload_area.title": "Քաշիր ու նետիր՝ վերբեռնելու համար",
"upload_button.label": "Ավելացնել մեդիա", "upload_button.label": "Ավելացնել մեդիա",
"upload_error.limit": "Ֆայլի վերբեռնման սահմանաչափը գերազանցված է։", "upload_error.limit": "Ֆայլի վերբեռնման սահմանաչափը գերազանցված է։",
"upload_error.poll": "Հարցումների հետ ֆայլ կցել հնարավոր չէ։", "upload_error.poll": "Հարցումների հետ նիշք կցել հնարաւոր չէ։",
"upload_form.audio_description": "Նկարագրիր ձայնագրության բովանդակությունը լսողական խնդիրներով անձանց համար", "upload_form.audio_description": "Նկարագրիր ձայնագրութեան բովանդակութիւնը լսողական խնդիրներով անձանց համար",
"upload_form.description": "Նկարագրություն ավելացրու տեսողական խնդիրներ ունեցողների համար", "upload_form.description": "Նկարագիր՝ տեսողական խնդիրներ ունեցողների համար",
"upload_form.edit": "Խմբագրել", "upload_form.edit": "Խմբագրել",
"upload_form.undo": "Հետարկել", "upload_form.undo": "Հետարկել",
"upload_form.video_description": "Նկարագրիր տեսանյութը լսողական կամ տեսողական խնդիրներով անձանց համար", "upload_form.video_description": "Նկարագրիր տեսանիւթը լսողական կամ տեսողական խնդիրներով անձանց համար",
"upload_modal.analyzing_picture": "Լուսանկարի վերլուծում…", "upload_modal.analyzing_picture": "Լուսանկարի վերլուծում…",
"upload_modal.apply": "Կիրառել", "upload_modal.apply": "Կիրառել",
"upload_modal.description_placeholder": "Ճկուն շագանակագույն աղվեսը ցատկում է ծույլ շան վրայով", "upload_modal.description_placeholder": "Բել դղյակի ձախ ժամն օֆ ազգությանը ցպահանջ չճշտած վնաս էր եւ փառք։",
"upload_modal.detect_text": "Հայտնբերել տեքստը նկարից", "upload_modal.detect_text": "Հայտնբերել տեքստը նկարից",
"upload_modal.edit_media": "Խմբագրել մեդիան", "upload_modal.edit_media": "Խմբագրել մեդիան",
"upload_modal.hint": "Սեղմեք և տեղաշարժեք նախատեսքի վրայի շրջանակը ընտրելու այն կետը որը միշտ տեսանելի կլինի մանրապատկերներում։", "upload_modal.hint": "Սեղմէք եւ տեղաշարժէք նախադիտման շրջանակը՝ որ ընտրէք մանրապատկերում միշտ տեսանելի կէտը։",
"upload_modal.preview_label": "Նախադիտում ({ratio})", "upload_modal.preview_label": "Նախադիտում ({ratio})",
"upload_progress.label": "Վերբեռնվում է…", "upload_progress.label": "Վերբեռնվում է…",
"video.close": "Փակել տեսագրությունը", "video.close": "Փակել տեսագրութիւնը",
"video.download": "Ներբեռնել ֆայլը", "video.download": "Ներբեռնել ֆայլը",
"video.exit_fullscreen": "Անջատել լիաէկրան դիտումը", "video.exit_fullscreen": "Անջատել լիաէկրան դիտումը",
"video.expand": "Ընդարձակել տեսագրությունը", "video.expand": "Ընդարձակել տեսագրութիւնը",
"video.fullscreen": "Լիաէկրան", "video.fullscreen": "Լիաէկրան",
"video.hide": "Թաքցնել տեսագրությունը", "video.hide": "Թաքցնել տեսագրութիւնը",
"video.mute": "Լռեցնել ձայնը", "video.mute": "Լռեցնել ձայնը",
"video.pause": "Դադար տալ", "video.pause": "Դադար տալ",
"video.play": "Նվագել", "video.play": "Նվագել",

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