Merge commit '7866745a6bf21e5187137c321040ed58d37e4331' into bark-prod

This commit is contained in:
Dalite 2025-04-05 11:43:57 +02:00
commit 0dc3974f1c
403 changed files with 5878 additions and 2775 deletions
.dockerignore.env.production.sample
.github/workflows
.prettierignore.rubocop.yml
.rubocop
.rubocop_todo.ymlCHANGELOG.mdDockerfileGemfileGemfile.lock
app
controllers
helpers
javascript

View file

@ -20,3 +20,9 @@ postgres14
redis
elasticsearch
chart
.yarn/
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

View file

@ -86,3 +86,24 @@ S3_ALIAS_HOST=files.example.com
# -----------------------
IP_RETENTION_PERIOD=31556952
SESSION_RETENTION_PERIOD=31556952
# Fetch All Replies Behavior
# --------------------------
# When a user expands a post (DetailedStatus view), fetch all of its replies
# (default: false)
FETCH_REPLIES_ENABLED=false
# Period to wait between fetching replies (in minutes)
FETCH_REPLIES_COOLDOWN_MINUTES=15
# Period to wait after a post is first created before fetching its replies (in minutes)
FETCH_REPLIES_INITIAL_WAIT_MINUTES=5
# Max number of replies to fetch - total, recursively through a whole reply tree
FETCH_REPLIES_MAX_GLOBAL=1000
# Max number of replies to fetch - for a single post
FETCH_REPLIES_MAX_SINGLE=500
# Max number of replies Collection pages to fetch - total
FETCH_REPLIES_MAX_PAGES=500

View file

@ -43,4 +43,4 @@ jobs:
- name: Run haml-lint
run: |
echo "::add-matcher::.github/workflows/haml-lint-problem-matcher.json"
bin/haml-lint --parallel --reporter github
bin/haml-lint --reporter github

View file

@ -8,6 +8,7 @@ on:
- .github/workflows/test-image-build.yml
- Dockerfile
- streaming/Dockerfile
- .dockerignore
permissions:
contents: read

View file

@ -77,6 +77,18 @@ jobs:
- name: Set up Ruby environment
uses: ./.github/actions/setup-ruby
- name: Ensure no errors with `db:prepare`
run: |
bin/rails db:drop
bin/rails db:prepare
bin/rails db:migrate
- name: Ensure no errors with `db:prepare` and SKIP_POST_DEPLOYMENT_MIGRATIONS
run: |
bin/rails db:drop
SKIP_POST_DEPLOYMENT_MIGRATIONS=true bin/rails db:prepare
bin/rails db:migrate
- name: Test "one step migration" flow
run: |
bin/rails db:drop

View file

@ -63,6 +63,7 @@ docker-compose.override.yml
# Ignore emoji map file
/app/javascript/mastodon/features/emoji/emoji_map.json
/app/javascript/mastodon/features/emoji/emoji_sheet.json
# Ignore locale files
/app/javascript/mastodon/locales/*.json

View file

@ -18,6 +18,7 @@ inherit_from:
- .rubocop/rspec_rails.yml
- .rubocop/rspec.yml
- .rubocop/style.yml
- .rubocop/i18n.yml
- .rubocop/custom.yml
- .rubocop_todo.yml
- .rubocop/strict.yml
@ -27,10 +28,9 @@ inherit_mode:
- Exclude
plugins:
- rubocop-capybara
- rubocop-i18n
- rubocop-performance
- rubocop-rails
- rubocop-rspec
- rubocop-performance
require:
- rubocop-rspec_rails
- rubocop-capybara

12
.rubocop/i18n.yml Normal file
View file

@ -0,0 +1,12 @@
I18n/RailsI18n:
Enabled: true
Exclude:
- 'config/**/*'
- 'db/**/*'
- 'lib/**/*'
- 'spec/**/*'
I18n/GetText:
Enabled: false
I18n/RailsI18n/DecorateStringFormattingUsingInterpolation:
Enabled: false

View file

@ -1,6 +1,6 @@
# This configuration was generated by
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp`
# using RuboCop version 1.72.2.
# using RuboCop version 1.73.2.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new

View file

@ -2,6 +2,30 @@
All notable changes to this project will be documented in this file.
## [4.3.6] - 2025-03-13
### Security
- Update dependency `omniauth-saml`
- Update dependency `rack`
### Fixed
- Fix Stoplight errors when using `REDIS_NAMESPACE` (#34126 by @ClearlyClaire)
## [4.3.5] - 2025-03-10
### Changed
- Change hashtag suggestion to prefer personal history capitalization (#34070 by @ClearlyClaire)
### Fixed
- Fix processing errors for some HEIF images from iOS 18 (#34086 by @renchap)
- Fix streaming server not filtering unknown-language posts from public timelines (#33774 by @ClearlyClaire)
- Fix preview cards under Content Warnings not being shown in detailed statuses (#34068 by @ClearlyClaire)
- Fix username and display name being hidden on narrow screens in moderation interface (#33064 by @ClearlyClaire)
## [4.3.4] - 2025-02-27
### Security

View file

@ -14,12 +14,12 @@ ARG BASE_REGISTRY="docker.io"
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.4.x"]
# renovate: datasource=docker depName=docker.io/ruby
ARG RUBY_VERSION="3.4.2"
# # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
# # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
# renovate: datasource=node-version depName=node
ARG NODE_MAJOR_VERSION="22"
# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"]
ARG DEBIAN_VERSION="bookworm"
# Node image to use for base image based on combined variables (ex: 20-bookworm-slim)
# Node.js image to use for base image based on combined variables (ex: 20-bookworm-slim)
FROM ${BASE_REGISTRY}/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim AS node
# Ruby image to use for base image based on combined variables (ex: 3.4.x-slim-bookworm)
FROM ${BASE_REGISTRY}/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} AS ruby
@ -61,7 +61,7 @@ ENV \
ENV \
# Configure the IP to bind Mastodon to when serving traffic
BIND="0.0.0.0" \
# Use production settings for Yarn, Node and related nodejs based tools
# Use production settings for Yarn, Node.js and related tools
NODE_ENV="production" \
# Use production settings for Ruby on Rails
RAILS_ENV="production" \
@ -96,6 +96,9 @@ RUN \
# Set /opt/mastodon as working directory
WORKDIR /opt/mastodon
# Add backport repository for some specific packages where we need the latest version
RUN echo 'deb http://deb.debian.org/debian bookworm-backports main' >> /etc/apt/sources.list
# hadolint ignore=DL3008,DL3005
RUN \
# Mount Apt cache and lib directories from Docker buildx caches
@ -125,13 +128,6 @@ RUN \
# Create temporary build layer from base image
FROM ruby AS build
# Copy Node package configuration files into working directory
COPY package.json yarn.lock .yarnrc.yml /opt/mastodon/
COPY .yarn /opt/mastodon/.yarn
COPY --from=node /usr/local/bin /usr/local/bin
COPY --from=node /usr/local/lib /usr/local/lib
ARG TARGETPLATFORM
# hadolint ignore=DL3008
@ -165,7 +161,7 @@ RUN \
libexif-dev \
libexpat1-dev \
libgirepository1.0-dev \
libheif-dev \
libheif-dev/bookworm-backports \
libimagequant-dev \
libjpeg62-turbo-dev \
liblcms2-dev \
@ -185,12 +181,6 @@ RUN \
libx265-dev \
;
RUN \
# Configure Corepack
rm /usr/local/bin/yarn*; \
corepack enable; \
corepack prepare --activate;
# Create temporary libvips specific build layer from build layer
FROM build AS libvips
@ -281,38 +271,37 @@ RUN \
# Download and install required Gems
bundle install -j"$(nproc)";
# Create temporary node specific build layer from build layer
FROM build AS yarn
# Create temporary assets build layer from build layer
FROM build AS precompiler
ARG TARGETPLATFORM
# Copy Node package configuration files into working directory
COPY package.json yarn.lock .yarnrc.yml /opt/mastodon/
COPY streaming/package.json /opt/mastodon/streaming/
COPY .yarn /opt/mastodon/.yarn
# Copy Mastodon sources into layer
COPY . /opt/mastodon/
# Copy Node.js binaries/libraries into layer
COPY --from=node /usr/local/bin /usr/local/bin
COPY --from=node /usr/local/lib /usr/local/lib
RUN \
# Configure Corepack
rm /usr/local/bin/yarn*; \
corepack enable; \
corepack prepare --activate;
# hadolint ignore=DL3008
RUN \
--mount=type=cache,id=corepack-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/corepack,sharing=locked \
--mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \
# Install Node packages
# Install Node.js packages
yarn workspaces focus --production @mastodon/mastodon;
# Create temporary assets build layer from build layer
FROM build AS precompiler
# Copy Mastodon sources into precompiler layer
COPY . /opt/mastodon/
# Copy bundler and node packages from build layer to container
COPY --from=yarn /opt/mastodon /opt/mastodon/
COPY --from=bundler /opt/mastodon /opt/mastodon/
COPY --from=bundler /usr/local/bundle/ /usr/local/bundle/
# Copy libvips components to layer for precompiler
# Copy libvips components into layer for precompiler
COPY --from=libvips /usr/local/libvips/bin /usr/local/bin
COPY --from=libvips /usr/local/libvips/lib /usr/local/lib
ARG TARGETPLATFORM
# Copy bundler packages into layer for precompiler
COPY --from=bundler /opt/mastodon /opt/mastodon/
COPY --from=bundler /usr/local/bundle/ /usr/local/bundle/
RUN \
ldconfig; \
@ -348,7 +337,7 @@ RUN \
# libvips components
libcgif0 \
libexif12 \
libheif1 \
libheif1/bookworm-backports \
libimagequant0 \
libjpeg62-turbo \
liblcms2-2 \

View file

@ -39,7 +39,7 @@ gem 'net-ldap', '~> 0.18'
gem 'omniauth', '~> 2.0'
gem 'omniauth-cas', '~> 3.0.0.beta.1'
gem 'omniauth_openid_connect', '~> 0.6.1'
gem 'omniauth_openid_connect', '~> 0.8.0'
gem 'omniauth-rails_csrf_protection', '~> 1.0'
gem 'omniauth-saml', '~> 2.0'
@ -102,10 +102,10 @@ gem 'rdf-normalize', '~> 0.5'
gem 'prometheus_exporter', '~> 2.2', require: false
gem 'opentelemetry-api', '~> 1.4.0'
gem 'opentelemetry-api', '~> 1.5.0'
group :opentelemetry do
gem 'opentelemetry-exporter-otlp', '~> 0.29.0', require: false
gem 'opentelemetry-exporter-otlp', '~> 0.30.0', require: false
gem 'opentelemetry-instrumentation-active_job', '~> 0.8.0', require: false
gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.22.0', require: false
gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.22.0', require: false
@ -165,6 +165,7 @@ group :development do
# Code linting CLI and plugins
gem 'rubocop', require: false
gem 'rubocop-capybara', require: false
gem 'rubocop-i18n', require: false
gem 'rubocop-performance', require: false
gem 'rubocop-rails', require: false
gem 'rubocop-rspec', require: false

View file

@ -10,29 +10,29 @@ GIT
GEM
remote: https://rubygems.org/
specs:
actioncable (8.0.1)
actionpack (= 8.0.1)
activesupport (= 8.0.1)
actioncable (8.0.2)
actionpack (= 8.0.2)
activesupport (= 8.0.2)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (8.0.1)
actionpack (= 8.0.1)
activejob (= 8.0.1)
activerecord (= 8.0.1)
activestorage (= 8.0.1)
activesupport (= 8.0.1)
actionmailbox (8.0.2)
actionpack (= 8.0.2)
activejob (= 8.0.2)
activerecord (= 8.0.2)
activestorage (= 8.0.2)
activesupport (= 8.0.2)
mail (>= 2.8.0)
actionmailer (8.0.1)
actionpack (= 8.0.1)
actionview (= 8.0.1)
activejob (= 8.0.1)
activesupport (= 8.0.1)
actionmailer (8.0.2)
actionpack (= 8.0.2)
actionview (= 8.0.2)
activejob (= 8.0.2)
activesupport (= 8.0.2)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (8.0.1)
actionview (= 8.0.1)
activesupport (= 8.0.1)
actionpack (8.0.2)
actionview (= 8.0.2)
activesupport (= 8.0.2)
nokogiri (>= 1.8.5)
rack (>= 2.2.4)
rack-session (>= 1.0.1)
@ -40,15 +40,15 @@ GEM
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (8.0.1)
actionpack (= 8.0.1)
activerecord (= 8.0.1)
activestorage (= 8.0.1)
activesupport (= 8.0.1)
actiontext (8.0.2)
actionpack (= 8.0.2)
activerecord (= 8.0.2)
activestorage (= 8.0.2)
activesupport (= 8.0.2)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (8.0.1)
activesupport (= 8.0.1)
actionview (8.0.2)
activesupport (= 8.0.2)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
@ -58,22 +58,22 @@ GEM
activemodel (>= 4.1)
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
activejob (8.0.1)
activesupport (= 8.0.1)
activejob (8.0.2)
activesupport (= 8.0.2)
globalid (>= 0.3.6)
activemodel (8.0.1)
activesupport (= 8.0.1)
activerecord (8.0.1)
activemodel (= 8.0.1)
activesupport (= 8.0.1)
activemodel (8.0.2)
activesupport (= 8.0.2)
activerecord (8.0.2)
activemodel (= 8.0.2)
activesupport (= 8.0.2)
timeout (>= 0.4.0)
activestorage (8.0.1)
actionpack (= 8.0.1)
activejob (= 8.0.1)
activerecord (= 8.0.1)
activesupport (= 8.0.1)
activestorage (8.0.2)
actionpack (= 8.0.2)
activejob (= 8.0.2)
activerecord (= 8.0.2)
activesupport (= 8.0.2)
marcel (~> 1.0)
activesupport (8.0.1)
activesupport (8.0.2)
base64
benchmark (>= 0.3)
bigdecimal
@ -217,6 +217,8 @@ GEM
htmlentities (~> 4.3.3)
launchy (>= 2.1, < 4.0)
mail (~> 2.7)
email_validator (2.2.4)
activemodel
erubi (1.13.1)
et-orbi (1.2.11)
tzinfo
@ -228,6 +230,8 @@ GEM
faraday-net_http (>= 2.0, < 3.5)
json
logger
faraday-follow_redirects (0.3.0)
faraday (>= 1, < 3)
faraday-httpclient (2.0.1)
httpclient (>= 2.2)
faraday-net_http (3.4.0)
@ -261,8 +265,8 @@ GEM
raabro (~> 1.4)
globalid (1.2.1)
activesupport (>= 6.1)
google-protobuf (3.25.5)
googleapis-common-protos-types (1.15.0)
google-protobuf (3.25.6)
googleapis-common-protos-types (1.18.0)
google-protobuf (>= 3.18, < 5.a)
haml (6.3.0)
temple (>= 0.8.2)
@ -304,7 +308,7 @@ GEM
rainbow (>= 2.0.0)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
i18n-tasks (1.0.14)
i18n-tasks (1.0.15)
activesupport (>= 4.0.2)
ast (>= 2.1.0)
erubi
@ -313,6 +317,7 @@ GEM
parser (>= 3.2.2.1)
rails-i18n
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.8, >= 1.8.1)
terminal-table (>= 1.5.1)
idn-ruby (0.1.5)
inline_svg (1.10.0)
@ -328,13 +333,15 @@ GEM
azure-blob (~> 0.5.2)
hashie (~> 5.0)
jmespath (1.6.2)
json (2.10.1)
json (2.10.2)
json-canonicalization (1.0.0)
json-jwt (1.15.3.1)
json-jwt (1.16.7)
activesupport (>= 4.2)
aes_key_wrap
base64
bindata
httpclient
faraday (~> 2.0)
faraday-follow_redirects
json-ld (3.3.2)
htmlentities (~> 4.3)
json-canonicalization (~> 1.0)
@ -435,41 +442,43 @@ GEM
oj (3.16.10)
bigdecimal (>= 3.0)
ostruct (>= 0.2)
omniauth (2.1.2)
omniauth (2.1.3)
hashie (>= 3.4.6)
rack (>= 2.2.3)
rack-protection
omniauth-cas (3.0.0)
omniauth-cas (3.0.1)
addressable (~> 2.8)
nokogiri (~> 1.12)
omniauth (~> 2.1)
omniauth-rails_csrf_protection (1.0.2)
actionpack (>= 4.2)
omniauth (~> 2.0)
omniauth-saml (2.2.1)
omniauth-saml (2.2.3)
omniauth (~> 2.1)
ruby-saml (~> 1.17)
omniauth_openid_connect (0.6.1)
ruby-saml (~> 1.18)
omniauth_openid_connect (0.8.0)
omniauth (>= 1.9, < 3)
openid_connect (~> 1.1)
openid_connect (1.4.2)
openid_connect (~> 2.2)
openid_connect (2.3.1)
activemodel
attr_required (>= 1.0.0)
json-jwt (>= 1.15.0)
net-smtp
rack-oauth2 (~> 1.21)
swd (~> 1.3)
email_validator
faraday (~> 2.0)
faraday-follow_redirects
json-jwt (>= 1.16)
mail
rack-oauth2 (~> 2.2)
swd (~> 2.0)
tzinfo
validate_email
validate_url
webfinger (~> 1.2)
webfinger (~> 2.0)
openssl (3.3.0)
openssl-signature_algorithm (1.3.0)
openssl (> 2.0)
opentelemetry-api (1.4.0)
opentelemetry-common (0.21.0)
opentelemetry-api (1.5.0)
opentelemetry-common (0.22.0)
opentelemetry-api (~> 1.0)
opentelemetry-exporter-otlp (0.29.1)
opentelemetry-exporter-otlp (0.30.0)
google-protobuf (>= 3.18)
googleapis-common-protos-types (~> 1.3)
opentelemetry-api (~> 1.1)
@ -500,8 +509,8 @@ GEM
opentelemetry-instrumentation-active_record (0.9.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-active_storage (0.1.0)
opentelemetry-api (~> 1.4.0)
opentelemetry-instrumentation-active_storage (0.1.1)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-active_support (~> 0.7)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-instrumentation-active_support (0.8.0)
@ -553,14 +562,14 @@ GEM
opentelemetry-instrumentation-sidekiq (0.26.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.23.0)
opentelemetry-registry (0.3.1)
opentelemetry-registry (0.4.0)
opentelemetry-api (~> 1.1)
opentelemetry-sdk (1.7.0)
opentelemetry-sdk (1.8.0)
opentelemetry-api (~> 1.1)
opentelemetry-common (~> 0.20)
opentelemetry-registry (~> 0.2)
opentelemetry-semantic_conventions
opentelemetry-semantic_conventions (1.10.1)
opentelemetry-semantic_conventions (1.11.0)
opentelemetry-api (~> 1.0)
orm_adapter (0.5.0)
ostruct (0.6.1)
@ -600,19 +609,20 @@ GEM
public_suffix (6.0.1)
puma (6.6.0)
nio4r (~> 2.0)
pundit (2.4.0)
pundit (2.5.0)
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.8.1)
rack (2.2.11)
rack (2.2.13)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-cors (2.0.2)
rack (>= 2.0.0)
rack-oauth2 (1.21.3)
rack-oauth2 (2.2.1)
activesupport
attr_required
httpclient
faraday (~> 2.0)
faraday-follow_redirects
json-jwt (>= 1.11.0)
rack (>= 2.1.0)
rack-protection (3.2.0)
@ -627,20 +637,20 @@ GEM
rackup (1.0.1)
rack (< 3)
webrick
rails (8.0.1)
actioncable (= 8.0.1)
actionmailbox (= 8.0.1)
actionmailer (= 8.0.1)
actionpack (= 8.0.1)
actiontext (= 8.0.1)
actionview (= 8.0.1)
activejob (= 8.0.1)
activemodel (= 8.0.1)
activerecord (= 8.0.1)
activestorage (= 8.0.1)
activesupport (= 8.0.1)
rails (8.0.2)
actioncable (= 8.0.2)
actionmailbox (= 8.0.2)
actionmailer (= 8.0.2)
actionpack (= 8.0.2)
actiontext (= 8.0.2)
actionview (= 8.0.2)
activejob (= 8.0.2)
activemodel (= 8.0.2)
activerecord (= 8.0.2)
activestorage (= 8.0.2)
activesupport (= 8.0.2)
bundler (>= 1.15.0)
railties (= 8.0.1)
railties (= 8.0.2)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
@ -651,9 +661,9 @@ GEM
rails-i18n (8.0.1)
i18n (>= 0.7, < 2)
railties (>= 8.0.0, < 9)
railties (8.0.1)
actionpack (= 8.0.1)
activesupport (= 8.0.1)
railties (8.0.2)
actionpack (= 8.0.2)
activesupport (= 8.0.2)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
@ -669,7 +679,7 @@ GEM
rdf (~> 3.3)
rdoc (6.12.0)
psych (>= 4.0.0)
redcarpet (3.6.0)
redcarpet (3.6.1)
redis (4.8.1)
redis-namespace (1.11.0)
redis (>= 4)
@ -713,13 +723,13 @@ GEM
rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13)
rspec-support (~> 3.13)
rspec-sidekiq (5.0.0)
rspec-sidekiq (5.1.0)
rspec-core (~> 3.0)
rspec-expectations (~> 3.0)
rspec-mocks (~> 3.0)
sidekiq (>= 5, < 8)
sidekiq (>= 5, < 9)
rspec-support (3.13.2)
rubocop (1.72.2)
rubocop (1.73.2)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
@ -730,15 +740,19 @@ GEM
rubocop-ast (>= 1.38.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.38.0)
rubocop-ast (1.38.1)
parser (>= 3.3.1.0)
rubocop-capybara (2.21.0)
rubocop (~> 1.41)
rubocop-capybara (2.22.1)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-i18n (3.2.3)
lint_roller (~> 1.1)
rubocop (>= 1.72.1)
rubocop-performance (1.24.0)
lint_roller (~> 1.1)
rubocop (>= 1.72.1, < 2.0)
rubocop-ast (>= 1.38.0, < 2.0)
rubocop-rails (2.30.2)
rubocop-rails (2.30.3)
activesupport (>= 4.2.0)
lint_roller (~> 1.1)
rack (>= 1.1)
@ -747,12 +761,13 @@ GEM
rubocop-rspec (3.5.0)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-rspec_rails (2.30.0)
rubocop (~> 1.61)
rubocop-rspec (~> 3, >= 3.0.1)
rubocop-rspec_rails (2.31.0)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-rspec (~> 3.5)
ruby-prof (1.7.1)
ruby-progressbar (1.13.0)
ruby-saml (1.17.0)
ruby-saml (1.18.0)
nokogiri (>= 1.13.10)
rexml
ruby-vips (2.2.3)
@ -810,13 +825,14 @@ GEM
stackprof (0.2.27)
stoplight (4.1.1)
redlock (~> 1.0)
stringio (3.1.4)
stringio (3.1.5)
strong_migrations (2.2.0)
activerecord (>= 7)
swd (1.3.0)
swd (2.0.3)
activesupport (>= 3)
attr_required (>= 0.0.5)
httpclient (>= 2.4)
faraday (~> 2.0)
faraday-follow_redirects
sysexits (1.2.0)
temple (0.10.3)
terminal-table (4.0.0)
@ -854,11 +870,8 @@ GEM
unicode-display_width (3.1.4)
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
uri (1.0.2)
uri (1.0.3)
useragent (0.16.11)
validate_email (0.1.6)
activemodel (>= 3.0)
mail (>= 2.2.5)
validate_url (1.0.15)
activemodel (>= 3.0.0)
public_suffix
@ -872,10 +885,11 @@ GEM
openssl (>= 2.2)
safety_net_attestation (~> 0.4.0)
tpm-key_attestation (~> 0.14.0)
webfinger (1.2.0)
webfinger (2.1.3)
activesupport
httpclient (>= 2.4)
webmock (3.25.0)
faraday (~> 2.0)
faraday-follow_redirects
webmock (3.25.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@ -973,9 +987,9 @@ DEPENDENCIES
omniauth-cas (~> 3.0.0.beta.1)
omniauth-rails_csrf_protection (~> 1.0)
omniauth-saml (~> 2.0)
omniauth_openid_connect (~> 0.6.1)
opentelemetry-api (~> 1.4.0)
opentelemetry-exporter-otlp (~> 0.29.0)
omniauth_openid_connect (~> 0.8.0)
opentelemetry-api (~> 1.5.0)
opentelemetry-exporter-otlp (~> 0.30.0)
opentelemetry-instrumentation-active_job (~> 0.8.0)
opentelemetry-instrumentation-active_model_serializers (~> 0.22.0)
opentelemetry-instrumentation-concurrent_ruby (~> 0.22.0)
@ -1016,6 +1030,7 @@ DEPENDENCIES
rspec-sidekiq (~> 5.0)
rubocop
rubocop-capybara
rubocop-i18n
rubocop-performance
rubocop-rails
rubocop-rspec

View file

@ -0,0 +1,18 @@
# frozen_string_literal: true
class Admin::Announcements::DistributionsController < Admin::BaseController
before_action :set_announcement
def create
authorize @announcement, :distribute?
@announcement.touch(:notification_sent_at)
Admin::DistributeAnnouncementNotificationWorker.perform_async(@announcement.id)
redirect_to admin_announcements_path
end
private
def set_announcement
@announcement = Announcement.find(params[:announcement_id])
end
end

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
class Admin::Announcements::PreviewsController < Admin::BaseController
before_action :set_announcement
def show
authorize @announcement, :distribute?
@user_count = @announcement.scope_for_notification.count
end
private
def set_announcement
@announcement = Announcement.find(params[:announcement_id])
end
end

View file

@ -0,0 +1,17 @@
# frozen_string_literal: true
class Admin::Announcements::TestsController < Admin::BaseController
before_action :set_announcement
def create
authorize @announcement, :distribute?
UserMailer.announcement_published(current_user, @announcement).deliver_later!
redirect_to admin_announcements_path
end
private
def set_announcement
@announcement = Announcement.find(params[:announcement_id])
end
end

View file

@ -23,7 +23,7 @@ class Admin::TermsOfService::DraftsController < Admin::BaseController
private
def set_terms_of_service
@terms_of_service = TermsOfService.draft.first || TermsOfService.new(text: current_terms_of_service&.text)
@terms_of_service = TermsOfService.draft.first || TermsOfService.new(text: current_terms_of_service&.text, effective_date: 10.days.from_now)
end
def current_terms_of_service
@ -32,6 +32,6 @@ class Admin::TermsOfService::DraftsController < Admin::BaseController
def resource_params
params
.expect(terms_of_service: [:text, :changelog])
.expect(terms_of_service: [:text, :changelog, :effective_date])
end
end

View file

@ -3,6 +3,6 @@
class Admin::TermsOfServiceController < Admin::BaseController
def index
authorize :terms_of_service, :index?
@terms_of_service = TermsOfService.live.first
@terms_of_service = TermsOfService.published.first
end
end

View file

@ -14,7 +14,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
@account = current_account
UpdateAccountService.new.call(@account, account_params, raise_error: true)
current_user.update(user_params) if user_params
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
render json: @account, serializer: REST::CredentialAccountSerializer
rescue ActiveRecord::RecordInvalid => e
render json: ValidationErrorFormatter.new(e).as_json, status: 422

View file

@ -5,12 +5,18 @@ class Api::V1::Instances::TermsOfServicesController < Api::V1::Instances::BaseCo
def show
cache_even_if_authenticated!
render json: @terms_of_service, serializer: REST::PrivacyPolicySerializer
render json: @terms_of_service, serializer: REST::TermsOfServiceSerializer
end
private
def set_terms_of_service
@terms_of_service = TermsOfService.live.first!
@terms_of_service = begin
if params[:date].present?
TermsOfService.published.find_by!(effective_date: params[:date])
else
TermsOfService.live.first || TermsOfService.published.first! # For the case when none of the published terms have become effective yet
end
end
end
end

View file

@ -3,8 +3,8 @@
class Api::V1::MediaController < Api::BaseController
before_action -> { doorkeeper_authorize! :write, :'write:media' }
before_action :require_user!
before_action :set_media_attachment, except: [:create]
before_action :check_processing, except: [:create]
before_action :set_media_attachment, except: [:create, :destroy]
before_action :check_processing, except: [:create, :destroy]
def show
render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment
@ -25,6 +25,15 @@ class Api::V1::MediaController < Api::BaseController
render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment
end
def destroy
@media_attachment = current_account.media_attachments.find(params[:id])
return render json: in_usage_error, status: 422 unless @media_attachment.status_id.nil?
@media_attachment.destroy
render_empty
end
private
def status_code_for_media_attachment
@ -54,4 +63,8 @@ class Api::V1::MediaController < Api::BaseController
def processing_error
{ error: 'Error processing thumbnail for uploaded media' }
end
def in_usage_error
{ error: 'Media attachment is currently used by a status' }
end
end

View file

@ -7,7 +7,7 @@ class Api::V1::Profile::AvatarsController < Api::BaseController
def destroy
@account = current_account
UpdateAccountService.new.call(@account, { avatar: nil }, raise_error: true)
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
render json: @account, serializer: REST::CredentialAccountSerializer
end
end

View file

@ -7,7 +7,7 @@ class Api::V1::Profile::HeadersController < Api::BaseController
def destroy
@account = current_account
UpdateAccountService.new.call(@account, { header: nil }, raise_error: true)
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
render json: @account, serializer: REST::CredentialAccountSerializer
end
end

View file

@ -58,6 +58,8 @@ class Api::V1::StatusesController < Api::BaseController
statuses = [@status] + @context.ancestors + @context.descendants
render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
ActivityPub::FetchAllRepliesWorker.perform_async(@status.id) if !current_account.nil? && @status.should_fetch_replies?
end
def create
@ -111,7 +113,7 @@ class Api::V1::StatusesController < Api::BaseController
@status.account.statuses_count = @status.account.statuses_count - 1
json = render_to_body json: @status, serializer: REST::StatusSerializer, source_requested: true
RemovalWorker.perform_async(@status.id, { 'redraft' => true })
RemovalWorker.perform_async(@status.id, { 'redraft' => !truthy_param?(:delete_media) })
render json: json
end

View file

@ -8,7 +8,7 @@ module Settings
def destroy
if valid_picture?
if UpdateAccountService.new.call(@account, { @picture => nil, "#{@picture}_remote_url" => '' })
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg'), status: 303
else
redirect_to settings_profile_path

View file

@ -8,7 +8,7 @@ class Settings::PrivacyController < Settings::BaseController
def update
if UpdateAccountService.new.call(@account, account_params.except(:settings))
current_user.update!(settings_attributes: account_params[:settings])
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
redirect_to settings_privacy_path, notice: I18n.t('generic.changes_saved_msg')
else
render :show

View file

@ -9,7 +9,7 @@ class Settings::ProfilesController < Settings::BaseController
def update
if UpdateAccountService.new.call(@account, account_params)
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg')
else
@account.build_fields

View file

@ -8,7 +8,7 @@ class Settings::VerificationsController < Settings::BaseController
def update
if UpdateAccountService.new.call(@account, account_params)
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
redirect_to settings_verification_path, notice: I18n.t('generic.changes_saved_msg')
else
render :show

View file

@ -155,24 +155,49 @@ module JsonLdHelper
end
end
def fetch_resource(uri, id_is_known, on_behalf_of = nil, request_options: {})
# Fetch the resource given by uri.
# @param uri [String]
# @param id_is_known [Boolean]
# @param on_behalf_of [nil, Account]
# @param raise_on_error [Symbol<:all, :temporary, :none>] See {#fetch_resource_without_id_validation} for possible values
def fetch_resource(uri, id_is_known, on_behalf_of = nil, raise_on_error: :none, request_options: {})
unless id_is_known
json = fetch_resource_without_id_validation(uri, on_behalf_of)
json = fetch_resource_without_id_validation(uri, on_behalf_of, raise_on_error: raise_on_error)
return if !json.is_a?(Hash) || unsupported_uri_scheme?(json['id'])
uri = json['id']
end
json = fetch_resource_without_id_validation(uri, on_behalf_of, request_options: request_options)
json = fetch_resource_without_id_validation(uri, on_behalf_of, raise_on_error: raise_on_error, request_options: request_options)
json.present? && json['id'] == uri ? json : nil
end
def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false, request_options: {})
# Fetch the resource given by uri
#
# If an error is raised, it contains the response and can be captured for handling like
#
# begin
# fetch_resource_without_id_validation(uri, nil, true)
# rescue Mastodon::UnexpectedResponseError => e
# e.response
# end
#
# @param uri [String]
# @param on_behalf_of [nil, Account]
# @param raise_on_error [Symbol<:all, :temporary, :none>]
# - +:all+ - raise if response code is not in the 2xx range
# - +:temporary+ - raise if the response code is not an "unsalvageable error" like a 404
# (see {#response_error_unsalvageable} )
# - +:none+ - do not raise, return +nil+
def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_error: :none, request_options: {})
on_behalf_of ||= Account.representative
build_request(uri, on_behalf_of, options: request_options).perform do |response|
raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error
raise Mastodon::UnexpectedResponseError, response if !response_successful?(response) && (
raise_on_error == :all ||
(!response_error_unsalvageable?(response) && raise_on_error == :temporary)
)
body_to_json(response.body_with_limit) if response.code == 200 && valid_activitypub_content_type?(response)
end

View file

@ -1,7 +1,7 @@
import './public-path';
import { createRoot } from 'react-dom/client';
import { afterInitialRender } from 'mastodon/../hooks/useRenderSignal';
import { afterInitialRender } from 'mastodon/hooks/useRenderSignal';
import { start } from '../mastodon/common';
import { Status } from '../mastodon/features/standalone/status';

View file

@ -29,7 +29,7 @@ const debouncedSave = debounce((dispatch, getState) => {
api().put('/api/web/settings', { data })
.then(() => dispatch({ type: SETTING_SAVE }))
.catch(error => dispatch(showAlertForError(error)));
}, 5000, { trailing: true });
}, 2000, { leading: true, trailing: true });
export function saveSettings() {
return (dispatch, getState) => debouncedSave(dispatch, getState);

View file

@ -138,7 +138,7 @@ export function deleteStatus(id, withRedraft = false) {
dispatch(deleteStatusRequest(id));
api().delete(`/api/v1/statuses/${id}`).then(response => {
api().delete(`/api/v1/statuses/${id}`, { params: { delete_media: !withRedraft } }).then(response => {
dispatch(deleteStatusSuccess(id));
dispatch(deleteFromTimelines(id));
dispatch(importFetchedAccount(response.data.account));

View file

@ -4,8 +4,12 @@ import type {
ApiPrivacyPolicyJSON,
} from 'mastodon/api_types/instance';
export const apiGetTermsOfService = () =>
apiRequestGet<ApiTermsOfServiceJSON>('v1/instance/terms_of_service');
export const apiGetTermsOfService = (version?: string) =>
apiRequestGet<ApiTermsOfServiceJSON>(
version
? `v1/instance/terms_of_service/${version}`
: 'v1/instance/terms_of_service',
);
export const apiGetPrivacyPolicy = () =>
apiRequestGet<ApiPrivacyPolicyJSON>('v1/instance/privacy_policy');

View file

@ -1,5 +1,7 @@
export interface ApiTermsOfServiceJSON {
updated_at: string;
effective_date: string;
effective: boolean;
succeeded_by: string | null;
content: string;
}

View file

@ -1,4 +1,4 @@
import { useLinks } from 'mastodon/../hooks/useLinks';
import { useLinks } from 'mastodon/hooks/useLinks';
export const AccountBio: React.FC<{
note: string;

View file

@ -1,8 +1,8 @@
import classNames from 'classnames';
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
import { useLinks } from 'mastodon/../hooks/useLinks';
import { Icon } from 'mastodon/components/icon';
import { useLinks } from 'mastodon/hooks/useLinks';
import type { Account } from 'mastodon/models/account';
export const AccountFields: React.FC<{

View file

@ -8,7 +8,7 @@ import type {
UsePopperOptions,
} from 'react-overlays/esm/usePopper';
import { useSelectableClick } from '@/hooks/useSelectableClick';
import { useSelectableClick } from 'mastodon/hooks/useSelectableClick';
const offset = [0, 4] as OffsetValue;
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;

View file

@ -2,7 +2,7 @@ import { useState, useCallback } from 'react';
import classNames from 'classnames';
import { useHovering } from 'mastodon/../hooks/useHovering';
import { useHovering } from 'mastodon/hooks/useHovering';
import { autoPlayGif } from 'mastodon/initial_state';
import type { Account } from 'mastodon/models/account';

View file

@ -1,8 +1,7 @@
import { useHovering } from 'mastodon/hooks/useHovering';
import { autoPlayGif } from 'mastodon/initial_state';
import type { Account } from 'mastodon/models/account';
import { useHovering } from '../../hooks/useHovering';
import { autoPlayGif } from '../initial_state';
interface Props {
account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
friend: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there

View file

@ -5,8 +5,8 @@ import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react';
import { useTimeout } from 'mastodon/../hooks/useTimeout';
import { Icon } from 'mastodon/components/icon';
import { useTimeout } from 'mastodon/hooks/useTimeout';
export const CopyPasteText: React.FC<{ value: string }> = ({ value }) => {
const inputRef = useRef<HTMLTextAreaElement>(null);

View file

@ -1,4 +1,4 @@
import { useHovering } from '@/hooks/useHovering';
import { useHovering } from 'mastodon/hooks/useHovering';
import { autoPlayGif } from 'mastodon/initial_state';
export const GIF: React.FC<{

View file

@ -151,7 +151,7 @@ export const Hashtag: React.FC<HashtagProps> = ({
<Sparklines
width={50}
height={28}
data={history ? history : Array.from(Array(7)).map(() => 0)}
data={history ?? Array.from(Array(7)).map(() => 0)}
>
<SparklinesCurve style={{ fill: 'none' }} />
</Sparklines>

View file

@ -8,8 +8,8 @@ import type {
UsePopperOptions,
} from 'react-overlays/esm/usePopper';
import { useTimeout } from 'mastodon/../hooks/useTimeout';
import { HoverCardAccount } from 'mastodon/components/hover_card_account';
import { useTimeout } from 'mastodon/hooks/useTimeout';
const offset = [-12, 4] as OffsetValue;
const enterDelay = 750;

View file

@ -149,6 +149,7 @@ export class IconButton extends PureComponent<Props, States> {
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
onKeyDown={this.handleKeyDown}
// eslint-disable-next-line @typescript-eslint/no-deprecated
onKeyPress={this.handleKeyPress}
style={style}
tabIndex={tabIndex}

View file

@ -81,6 +81,7 @@ class ScrollableList extends PureComponent {
bindToDocument: PropTypes.bool,
preventScroll: PropTypes.bool,
footer: PropTypes.node,
className: PropTypes.string,
};
static defaultProps = {
@ -325,7 +326,7 @@ class ScrollableList extends PureComponent {
};
render () {
const { children, scrollKey, trackScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, footer, emptyMessage, onLoadMore } = this.props;
const { children, scrollKey, className, trackScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, footer, emptyMessage, onLoadMore } = this.props;
const { fullscreen } = this.state;
const childrenCount = Children.count(children);
@ -336,9 +337,9 @@ class ScrollableList extends PureComponent {
if (showLoading) {
scrollableArea = (
<div className='scrollable scrollable--flex' ref={this.setRef}>
<div role='feed' className='item-list'>
{prepend}
</div>
{prepend}
<div role='feed' className='item-list' />
<div className='scrollable__append'>
<LoadingIndicator />
@ -350,9 +351,9 @@ class ScrollableList extends PureComponent {
} else if (isLoading || childrenCount > 0 || numPending > 0 || hasMore || !emptyMessage) {
scrollableArea = (
<div className={classNames('scrollable scrollable--flex', { fullscreen })} ref={this.setRef} onMouseMove={this.handleMouseMove}>
<div role='feed' className='item-list'>
{prepend}
{prepend}
<div role='feed' className={classNames('item-list', className)}>
{loadPending}
{Children.map(this.props.children, (child, index) => (

View file

@ -11,11 +11,15 @@ import { Icon } from 'mastodon/components/icon';
import { formatTime } from 'mastodon/features/video';
import { autoPlayGif, displayMedia, useBlurhash } from 'mastodon/initial_state';
import type { Status, MediaAttachment } from 'mastodon/models/status';
import { useAppSelector } from 'mastodon/store';
export const MediaItem: React.FC<{
attachment: MediaAttachment;
onOpenMedia: (arg0: MediaAttachment) => void;
}> = ({ attachment, onOpenMedia }) => {
const account = useAppSelector((state) =>
state.accounts.get(attachment.getIn(['status', 'account']) as string),
);
const [visible, setVisible] = useState(
(displayMedia !== 'hide_all' &&
!attachment.getIn(['status', 'sensitive'])) ||
@ -66,11 +70,10 @@ export const MediaItem: React.FC<{
attachment.get('description')) as string | undefined;
const previewUrl = attachment.get('preview_url') as string;
const fullUrl = attachment.get('url') as string;
const avatarUrl = status.getIn(['account', 'avatar_static']) as string;
const avatarUrl = account?.avatar_static;
const lang = status.get('language') as string;
const blurhash = attachment.get('blurhash') as string;
const statusId = status.get('id') as string;
const acct = status.getIn(['account', 'acct']) as string;
const type = attachment.get('type') as string;
let thumbnail;
@ -181,7 +184,7 @@ export const MediaItem: React.FC<{
<a
className='media-gallery__item-thumbnail'
href={`/@${acct}/${statusId}`}
href={`/@${account?.acct}/${statusId}`}
onClick={handleClick}
target='_blank'
rel='noopener noreferrer'

View file

@ -1,241 +0,0 @@
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { lookupAccount, fetchAccount } from 'mastodon/actions/accounts';
import { openModal } from 'mastodon/actions/modal';
import { ColumnBackButton } from 'mastodon/components/column_back_button';
import { LoadMore } from 'mastodon/components/load_more';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import ScrollContainer from 'mastodon/containers/scroll_container';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
import { getAccountGallery } from 'mastodon/selectors';
import { expandAccountMediaTimeline } from '../../actions/timelines';
import { AccountHeader } from '../account_timeline/components/account_header';
import Column from '../ui/components/column';
import { MediaItem } from './components/media_item';
const mapStateToProps = (state, { params: { acct, id } }) => {
const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]);
if (!accountId) {
return {
isLoading: true,
};
}
return {
accountId,
isAccount: !!state.getIn(['accounts', accountId]),
attachments: getAccountGallery(state, accountId),
isLoading: state.getIn(['timelines', `account:${accountId}:media`, 'isLoading']),
hasMore: state.getIn(['timelines', `account:${accountId}:media`, 'hasMore']),
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
};
};
class LoadMoreMedia extends ImmutablePureComponent {
static propTypes = {
maxId: PropTypes.string,
onLoadMore: PropTypes.func.isRequired,
};
handleLoadMore = () => {
this.props.onLoadMore(this.props.maxId);
};
render () {
return (
<LoadMore
disabled={this.props.disabled}
onClick={this.handleLoadMore}
/>
);
}
}
class AccountGallery extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.shape({
acct: PropTypes.string,
id: PropTypes.string,
}).isRequired,
accountId: PropTypes.string,
dispatch: PropTypes.func.isRequired,
attachments: ImmutablePropTypes.list.isRequired,
isLoading: PropTypes.bool,
hasMore: PropTypes.bool,
isAccount: PropTypes.bool,
blockedBy: PropTypes.bool,
suspended: PropTypes.bool,
multiColumn: PropTypes.bool,
};
state = {
width: 323,
};
_load () {
const { accountId, isAccount, dispatch } = this.props;
if (!isAccount) dispatch(fetchAccount(accountId));
dispatch(expandAccountMediaTimeline(accountId));
}
componentDidMount () {
const { params: { acct }, accountId, dispatch } = this.props;
if (accountId) {
this._load();
} else {
dispatch(lookupAccount(acct));
}
}
componentDidUpdate (prevProps) {
const { params: { acct }, accountId, dispatch } = this.props;
if (prevProps.accountId !== accountId && accountId) {
this._load();
} else if (prevProps.params.acct !== acct) {
dispatch(lookupAccount(acct));
}
}
handleScrollToBottom = () => {
if (this.props.hasMore) {
this.handleLoadMore(this.props.attachments.size > 0 ? this.props.attachments.last().getIn(['status', 'id']) : undefined);
}
};
handleScroll = e => {
const { scrollTop, scrollHeight, clientHeight } = e.target;
const offset = scrollHeight - scrollTop - clientHeight;
if (150 > offset && !this.props.isLoading) {
this.handleScrollToBottom();
}
};
handleLoadMore = maxId => {
this.props.dispatch(expandAccountMediaTimeline(this.props.accountId, { maxId }));
};
handleLoadOlder = e => {
e.preventDefault();
this.handleScrollToBottom();
};
handleOpenMedia = attachment => {
const { dispatch } = this.props;
const statusId = attachment.getIn(['status', 'id']);
const lang = attachment.getIn(['status', 'language']);
if (attachment.get('type') === 'video') {
dispatch(openModal({
modalType: 'VIDEO',
modalProps: { media: attachment, statusId, lang, options: { autoPlay: true } },
}));
} else if (attachment.get('type') === 'audio') {
dispatch(openModal({
modalType: 'AUDIO',
modalProps: { media: attachment, statusId, lang, options: { autoPlay: true } },
}));
} else {
const media = attachment.getIn(['status', 'media_attachments']);
const index = media.findIndex(x => x.get('id') === attachment.get('id'));
dispatch(openModal({
modalType: 'MEDIA',
modalProps: { media, index, statusId, lang },
}));
}
};
handleRef = c => {
if (c) {
this.setState({ width: c.offsetWidth });
}
};
render () {
const { attachments, isLoading, hasMore, isAccount, multiColumn, blockedBy, suspended } = this.props;
const { width } = this.state;
if (!isAccount) {
return (
<BundleColumnError multiColumn={multiColumn} errorType='routing' />
);
}
if (!attachments && isLoading) {
return (
<Column>
<LoadingIndicator />
</Column>
);
}
let loadOlder = null;
if (hasMore && !(isLoading && attachments.size === 0)) {
loadOlder = <LoadMore visible={!isLoading} onClick={this.handleLoadOlder} />;
}
let emptyMessage;
if (suspended) {
emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
} else if (blockedBy) {
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
}
return (
<Column>
<ColumnBackButton />
<ScrollContainer scrollKey='account_gallery'>
<div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
<AccountHeader accountId={this.props.accountId} />
{(suspended || blockedBy) ? (
<div className='empty-column-indicator'>
{emptyMessage}
</div>
) : (
<div role='feed' className='account-gallery__container' ref={this.handleRef}>
{attachments.map((attachment, index) => attachment === null ? (
<LoadMoreMedia key={'more:' + attachments.getIn(index + 1, 'id')} maxId={index > 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} />
) : (
<MediaItem key={attachment.get('id')} attachment={attachment} displayWidth={width} onOpenMedia={this.handleOpenMedia} />
))}
{loadOlder}
</div>
)}
{isLoading && attachments.size === 0 && (
<div className='scrollable__append'>
<LoadingIndicator />
</div>
)}
</div>
</ScrollContainer>
</Column>
);
}
}
export default connect(mapStateToProps)(AccountGallery);

View file

@ -0,0 +1,283 @@
import { useEffect, useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import { useParams } from 'react-router-dom';
import { createSelector } from '@reduxjs/toolkit';
import type { Map as ImmutableMap } from 'immutable';
import { List as ImmutableList } from 'immutable';
import { lookupAccount, fetchAccount } from 'mastodon/actions/accounts';
import { openModal } from 'mastodon/actions/modal';
import { expandAccountMediaTimeline } from 'mastodon/actions/timelines';
import { ColumnBackButton } from 'mastodon/components/column_back_button';
import ScrollableList from 'mastodon/components/scrollable_list';
import { TimelineHint } from 'mastodon/components/timeline_hint';
import { AccountHeader } from 'mastodon/features/account_timeline/components/account_header';
import { LimitedAccountHint } from 'mastodon/features/account_timeline/components/limited_account_hint';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import Column from 'mastodon/features/ui/components/column';
import type { MediaAttachment } from 'mastodon/models/media_attachment';
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
import { getAccountHidden } from 'mastodon/selectors/accounts';
import type { RootState } from 'mastodon/store';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { MediaItem } from './components/media_item';
const getAccountGallery = createSelector(
[
(state: RootState, accountId: string) =>
(state.timelines as ImmutableMap<string, unknown>).getIn(
[`account:${accountId}:media`, 'items'],
ImmutableList(),
) as ImmutableList<string>,
(state: RootState) => state.statuses,
],
(statusIds, statuses) => {
let items = ImmutableList<MediaAttachment>();
statusIds.forEach((statusId) => {
const status = statuses.get(statusId) as
| ImmutableMap<string, unknown>
| undefined;
if (status) {
items = items.concat(
(
status.get('media_attachments') as ImmutableList<MediaAttachment>
).map((media) => media.set('status', status)),
);
}
});
return items;
},
);
interface Params {
acct?: string;
id?: string;
}
const RemoteHint: React.FC<{
accountId: string;
}> = ({ accountId }) => {
const account = useAppSelector((state) => state.accounts.get(accountId));
const acct = account?.acct;
const url = account?.url;
const domain = acct ? acct.split('@')[1] : undefined;
if (!url) {
return null;
}
return (
<TimelineHint
url={url}
message={
<FormattedMessage
id='hints.profiles.posts_may_be_missing'
defaultMessage='Some posts from this profile may be missing.'
/>
}
label={
<FormattedMessage
id='hints.profiles.see_more_posts'
defaultMessage='See more posts on {domain}'
values={{ domain: <strong>{domain}</strong> }}
/>
}
/>
);
};
export const AccountGallery: React.FC<{
multiColumn: boolean;
}> = ({ multiColumn }) => {
const { acct, id } = useParams<Params>();
const dispatch = useAppDispatch();
const accountId = useAppSelector(
(state) =>
id ??
(state.accounts_map.get(normalizeForLookup(acct)) as string | undefined),
);
const attachments = useAppSelector((state) =>
accountId
? getAccountGallery(state, accountId)
: ImmutableList<MediaAttachment>(),
);
const isLoading = useAppSelector((state) =>
(state.timelines as ImmutableMap<string, unknown>).getIn([
`account:${accountId}:media`,
'isLoading',
]),
);
const hasMore = useAppSelector((state) =>
(state.timelines as ImmutableMap<string, unknown>).getIn([
`account:${accountId}:media`,
'hasMore',
]),
);
const account = useAppSelector((state) =>
accountId ? state.accounts.get(accountId) : undefined,
);
const blockedBy = useAppSelector(
(state) =>
state.relationships.getIn([accountId, 'blocked_by'], false) as boolean,
);
const suspended = useAppSelector(
(state) => state.accounts.getIn([accountId, 'suspended'], false) as boolean,
);
const isAccount = !!account;
const remote = account?.acct !== account?.username;
const hidden = useAppSelector((state) =>
accountId ? getAccountHidden(state, accountId) : false,
);
const maxId = attachments.last()?.getIn(['status', 'id']) as
| string
| undefined;
useEffect(() => {
if (!accountId) {
dispatch(lookupAccount(acct));
}
}, [dispatch, accountId, acct]);
useEffect(() => {
if (accountId && !isAccount) {
dispatch(fetchAccount(accountId));
}
if (accountId && isAccount) {
void dispatch(expandAccountMediaTimeline(accountId));
}
}, [dispatch, accountId, isAccount]);
const handleLoadMore = useCallback(() => {
if (maxId) {
void dispatch(expandAccountMediaTimeline(accountId, { maxId }));
}
}, [dispatch, accountId, maxId]);
const handleOpenMedia = useCallback(
(attachment: MediaAttachment) => {
const statusId = attachment.getIn(['status', 'id']);
const lang = attachment.getIn(['status', 'language']);
if (attachment.get('type') === 'video') {
dispatch(
openModal({
modalType: 'VIDEO',
modalProps: {
media: attachment,
statusId,
lang,
options: { autoPlay: true },
},
}),
);
} else if (attachment.get('type') === 'audio') {
dispatch(
openModal({
modalType: 'AUDIO',
modalProps: {
media: attachment,
statusId,
lang,
options: { autoPlay: true },
},
}),
);
} else {
const media = attachment.getIn([
'status',
'media_attachments',
]) as ImmutableList<MediaAttachment>;
const index = media.findIndex(
(x) => x.get('id') === attachment.get('id'),
);
dispatch(
openModal({
modalType: 'MEDIA',
modalProps: { media, index, statusId, lang },
}),
);
}
},
[dispatch],
);
if (accountId && !isAccount) {
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
}
let emptyMessage;
if (accountId) {
if (suspended) {
emptyMessage = (
<FormattedMessage
id='empty_column.account_suspended'
defaultMessage='Account suspended'
/>
);
} else if (hidden) {
emptyMessage = <LimitedAccountHint accountId={accountId} />;
} else if (blockedBy) {
emptyMessage = (
<FormattedMessage
id='empty_column.account_unavailable'
defaultMessage='Profile unavailable'
/>
);
} else if (remote && attachments.isEmpty()) {
emptyMessage = <RemoteHint accountId={accountId} />;
} else {
emptyMessage = (
<FormattedMessage
id='empty_column.account_timeline'
defaultMessage='No posts found'
/>
);
}
}
const forceEmptyState = suspended || blockedBy || hidden;
return (
<Column>
<ColumnBackButton />
<ScrollableList
className='account-gallery__container'
prepend={
accountId && (
<AccountHeader accountId={accountId} hideTabs={forceEmptyState} />
)
}
alwaysPrepend
append={remote && accountId && <RemoteHint accountId={accountId} />}
scrollKey='account_gallery'
isLoading={isLoading}
hasMore={!forceEmptyState && hasMore}
onLoadMore={handleLoadMore}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>
{attachments.map((attachment) => (
<MediaItem
key={attachment.get('id') as string}
attachment={attachment}
onOpenMedia={handleOpenMedia}
/>
))}
</ScrollableList>
</Column>
);
};
// eslint-disable-next-line import/no-default-export
export default AccountGallery;

View file

@ -6,7 +6,6 @@ import classNames from 'classnames';
import { Helmet } from 'react-helmet';
import { NavLink } from 'react-router-dom';
import { useLinks } from '@/hooks/useLinks';
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
@ -46,6 +45,7 @@ import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import { DomainPill } from 'mastodon/features/account/components/domain_pill';
import AccountNoteContainer from 'mastodon/features/account/containers/account_note_container';
import FollowRequestNoteContainer from 'mastodon/features/account/containers/follow_request_note_container';
import { useLinks } from 'mastodon/hooks/useLinks';
import { useIdentity } from 'mastodon/identity_context';
import { autoPlayGif, me, domain as localDomain } from 'mastodon/initial_state';
import type { Account } from 'mastodon/models/account';
@ -58,8 +58,8 @@ import {
import { getAccountHidden } from 'mastodon/selectors/accounts';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
import MemorialNote from './memorial_note';
import MovedNote from './moved_note';
import { MemorialNote } from './memorial_note';
import { MovedNote } from './moved_note';
const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
@ -833,7 +833,7 @@ export const AccountHeader: React.FC<{
<div className='account-timeline__header'>
{!hidden && account.memorial && <MemorialNote />}
{!hidden && account.moved && (
<MovedNote from={account} to={account.moved} />
<MovedNote accountId={account.id} targetAccountId={account.moved} />
)}
<div

View file

@ -1,11 +1,12 @@
import { FormattedMessage } from 'react-intl';
const MemorialNote = () => (
export const MemorialNote: React.FC = () => (
<div className='account-memorial-banner'>
<div className='account-memorial-banner__message'>
<FormattedMessage id='account.in_memoriam' defaultMessage='In Memoriam.' />
<FormattedMessage
id='account.in_memoriam'
defaultMessage='In Memoriam.'
/>
</div>
</div>
);
export default MemorialNote;

View file

@ -1,39 +0,0 @@
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { AvatarOverlay } from '../../../components/avatar_overlay';
import { DisplayName } from '../../../components/display_name';
export default class MovedNote extends ImmutablePureComponent {
static propTypes = {
from: ImmutablePropTypes.map.isRequired,
to: ImmutablePropTypes.map.isRequired,
};
render () {
const { from, to } = this.props;
return (
<div className='moved-account-banner'>
<div className='moved-account-banner__message'>
<FormattedMessage id='account.moved_to' defaultMessage='{name} has indicated that their new account is now:' values={{ name: <bdi><strong dangerouslySetInnerHTML={{ __html: from.get('display_name_html') }} /></bdi> }} />
</div>
<div className='moved-account-banner__action'>
<Link to={`/@${to.get('acct')}`} className='detailed-status__display-name'>
<div className='detailed-status__display-avatar'><AvatarOverlay account={to} friend={from} /></div>
<DisplayName account={to} />
</Link>
<Link to={`/@${to.get('acct')}`} className='button'><FormattedMessage id='account.go_to_profile' defaultMessage='Go to profile' /></Link>
</div>
</div>
);
}
}

View file

@ -0,0 +1,53 @@
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import { AvatarOverlay } from 'mastodon/components/avatar_overlay';
import { DisplayName } from 'mastodon/components/display_name';
import { useAppSelector } from 'mastodon/store';
export const MovedNote: React.FC<{
accountId: string;
targetAccountId: string;
}> = ({ accountId, targetAccountId }) => {
const from = useAppSelector((state) => state.accounts.get(accountId));
const to = useAppSelector((state) => state.accounts.get(targetAccountId));
return (
<div className='moved-account-banner'>
<div className='moved-account-banner__message'>
<FormattedMessage
id='account.moved_to'
defaultMessage='{name} has indicated that their new account is now:'
values={{
name: (
<bdi>
<strong
dangerouslySetInnerHTML={{
__html: from?.display_name_html ?? '',
}}
/>
</bdi>
),
}}
/>
</div>
<div className='moved-account-banner__action'>
<Link to={`/@${to?.acct}`} className='detailed-status__display-name'>
<div className='detailed-status__display-avatar'>
<AvatarOverlay account={to} friend={from} />
</div>
<DisplayName account={to} />
</Link>
<Link to={`/@${to?.acct}`} className='button'>
<FormattedMessage
id='account.go_to_profile'
defaultMessage='Go to profile'
/>
</Link>
</div>
</div>
);
};

View file

@ -6,9 +6,9 @@ import classNames from 'classnames';
import Overlay from 'react-overlays/Overlay';
import { useSelectableClick } from '@/hooks/useSelectableClick';
import QuestionMarkIcon from '@/material-icons/400-24px/question_mark.svg?react';
import { Icon } from 'mastodon/components/icon';
import { useSelectableClick } from 'mastodon/hooks/useSelectableClick';
const messages = defineMessages({
help: { id: 'info_button.label', defaultMessage: 'Help' },

View file

@ -1,4 +1,4 @@
import { useCallback } from 'react';
import { useMemo } from 'react';
import { defineMessages, useIntl } from 'react-intl';
@ -28,28 +28,30 @@ export const ActionBar = () => {
const dispatch = useDispatch();
const intl = useIntl();
const handleLogoutClick = useCallback(() => {
dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT' }));
}, [dispatch]);
const menu = useMemo(() => {
const handleLogoutClick = () => {
dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT' }));
};
let menu = [];
menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' });
menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' });
menu.push(null);
menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' });
menu.push({ text: intl.formatMessage(messages.bookmarks), to: '/bookmarks' });
menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' });
menu.push({ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' });
menu.push(null);
menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
menu.push({ text: intl.formatMessage(messages.filters), href: '/filters' });
menu.push(null);
menu.push({ text: intl.formatMessage(messages.logout), action: handleLogoutClick });
return ([
{ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' },
{ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' },
{ text: intl.formatMessage(messages.pins), to: '/pinned' },
null,
{ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' },
{ text: intl.formatMessage(messages.favourites), to: '/favourites' },
{ text: intl.formatMessage(messages.bookmarks), to: '/bookmarks' },
{ text: intl.formatMessage(messages.lists), to: '/lists' },
{ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' },
null,
{ text: intl.formatMessage(messages.mutes), to: '/mutes' },
{ text: intl.formatMessage(messages.blocks), to: '/blocks' },
{ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' },
{ text: intl.formatMessage(messages.filters), href: '/filters' },
null,
{ text: intl.formatMessage(messages.logout), action: handleLogoutClick },
]);
}, [intl, dispatch]);
return (
<DropdownMenuContainer

View file

@ -120,7 +120,7 @@ class ComposeForm extends ImmutablePureComponent {
return;
}
this.props.onSubmit(missingAltTextModal && this.props.missingAltText);
this.props.onSubmit(missingAltTextModal && this.props.missingAltText && this.props.privacy !== 'direct');
if (e) {
e.preventDefault();

View file

@ -12,11 +12,14 @@ import Overlay from 'react-overlays/Overlay';
import MoodIcon from '@/material-icons/400-20px/mood.svg?react';
import { IconButton } from 'mastodon/components/icon_button';
import emojiCompressed from 'mastodon/features/emoji/emoji_compressed';
import { assetHost } from 'mastodon/utils/config';
import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji';
import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
const nimblePickerData = emojiCompressed[5];
const messages = defineMessages({
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' },
@ -37,15 +40,18 @@ let EmojiPicker, Emoji; // load asynchronously
const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
const backgroundImageFn = () => `${assetHost}/emoji/sheet_13.png`;
const backgroundImageFn = () => `${assetHost}/emoji/sheet_15.png`;
const notFoundFn = () => (
<div className='emoji-mart-no-results'>
<Emoji
data={nimblePickerData}
emoji='sleuth_or_spy'
set='twitter'
size={32}
sheetSize={32}
sheetColumns={62}
sheetRows={62}
backgroundImageFn={backgroundImageFn}
/>
@ -67,7 +73,7 @@ class ModifierPickerMenu extends PureComponent {
this.props.onSelect(e.currentTarget.getAttribute('data-index') * 1);
};
UNSAFE_componentWillReceiveProps (nextProps) {
UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.active) {
this.attachListeners();
} else {
@ -75,7 +81,7 @@ class ModifierPickerMenu extends PureComponent {
}
}
componentWillUnmount () {
componentWillUnmount() {
this.removeListeners();
}
@ -85,12 +91,12 @@ class ModifierPickerMenu extends PureComponent {
}
};
attachListeners () {
attachListeners() {
document.addEventListener('click', this.handleDocumentClick, { capture: true });
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
}
removeListeners () {
removeListeners() {
document.removeEventListener('click', this.handleDocumentClick, { capture: true });
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
}
@ -99,17 +105,17 @@ class ModifierPickerMenu extends PureComponent {
this.node = c;
};
render () {
render() {
const { active } = this.props;
return (
<div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}>
<button type='button' onClick={this.handleClick} data-index={1}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={2}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={3}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={4}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={5}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={1}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={2}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={3}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={4}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={5}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} /></button>
<button type='button' onClick={this.handleClick} data-index={6}><Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} /></button>
</div>
);
}
@ -139,12 +145,12 @@ class ModifierPicker extends PureComponent {
this.props.onClose();
};
render () {
render() {
const { active, modifier } = this.props;
return (
<div className='emoji-picker-dropdown__modifiers'>
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} />
<Emoji data={nimblePickerData} sheetColumns={62} sheetRows={62} emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} />
<ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} />
</div>
);
@ -184,7 +190,7 @@ class EmojiPickerMenuImpl extends PureComponent {
}
};
componentDidMount () {
componentDidMount() {
document.addEventListener('click', this.handleDocumentClick, { capture: true });
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
@ -199,7 +205,7 @@ class EmojiPickerMenuImpl extends PureComponent {
});
}
componentWillUnmount () {
componentWillUnmount() {
document.removeEventListener('click', this.handleDocumentClick, { capture: true });
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
}
@ -252,7 +258,7 @@ class EmojiPickerMenuImpl extends PureComponent {
this.props.onSkinTone(modifier);
};
render () {
render() {
const { loading, style, intl, custom_emojis, skinTone, frequentlyUsedEmojis } = this.props;
if (loading) {
@ -280,6 +286,9 @@ class EmojiPickerMenuImpl extends PureComponent {
return (
<div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}>
<EmojiPicker
data={nimblePickerData}
sheetColumns={62}
sheetRows={62}
perLine={8}
emojiSize={22}
sheetSize={32}
@ -345,7 +354,7 @@ class EmojiPickerDropdown extends PureComponent {
EmojiPickerAsync().then(EmojiMart => {
EmojiPicker = EmojiMart.Picker;
Emoji = EmojiMart.Emoji;
Emoji = EmojiMart.Emoji;
this.setState({ loading: false });
}).catch(() => {
@ -386,7 +395,7 @@ class EmojiPickerDropdown extends PureComponent {
this.setState({ placement: state.placement });
};
render () {
render() {
const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props;
const title = intl.formatMessage(messages.emoji);
const { active, loading, placement } = this.state;
@ -403,7 +412,7 @@ class EmojiPickerDropdown extends PureComponent {
/>
<Overlay show={active} placement={placement} flip target={this.findTarget} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
{({ props, placement })=> (
{({ props, placement }) => (
<div {...props} style={{ ...props.style }}>
<div className={`dropdown-animation ${placement}`}>
<EmojiPickerMenu

View file

@ -22,10 +22,9 @@ import { LoadMore } from 'mastodon/components/load_more';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { RadioButton } from 'mastodon/components/radio_button';
import ScrollContainer from 'mastodon/containers/scroll_container';
import { useSearchParam } from 'mastodon/hooks/useSearchParam';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
import { useSearchParam } from '../../../hooks/useSearchParam';
import { AccountCard } from './components/account_card';
const messages = defineMessages({

View file

@ -45,6 +45,7 @@ type EmojiCompressed = [
Category[],
Data['aliases'],
EmojisWithoutShortCodes,
Data,
];
/*

View file

@ -9,18 +9,91 @@
// This version comment should be bumped each time the emoji data is changed
// to ensure that the prevaled file is regenerated by Babel
// version: 2
// version: 3
const { emojiIndex } = require('emoji-mart');
let data = require('emoji-mart/data/all.json');
// This json file contains the names of the categories.
const emojiMart5LocalesData = require('@emoji-mart/data/i18n/en.json');
const emojiMart5Data = require('@emoji-mart/data/sets/15/all.json');
const { uncompress: emojiMartUncompress } = require('emoji-mart/dist/utils/data');
const _ = require('lodash');
const emojiMap = require('./emoji_map.json');
// This json file is downloaded from https://github.com/iamcal/emoji-data/
// and is used to correct the sheet coordinates since we're using that repo's sheet
const emojiSheetData = require('./emoji_sheet.json');
const { unicodeToFilename } = require('./unicode_to_filename');
const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
if(data.compressed) {
data = emojiMartUncompress(data);
// Grabbed from `emoji_utils` to avoid circular dependency
function unifiedToNative(unified) {
let unicodes = unified.split('-'),
codePoints = unicodes.map((u) => `0x${u}`);
return String.fromCodePoint(...codePoints);
}
let data = {
compressed: true,
categories: emojiMart5Data.categories.map(cat => {
return {
...cat,
name: emojiMart5LocalesData.categories[cat.id]
};
}),
aliases: emojiMart5Data.aliases,
emojis: _(emojiMart5Data.emojis).values().map(emoji => {
let skin_variations = {};
const unified = emoji.skins[0].unified.toUpperCase();
const emojiFromRawData = emojiSheetData.find(e => e.unified === unified);
if (!emojiFromRawData) {
return undefined;
}
if (emoji.skins.length > 1) {
const [, ...nonDefaultSkins] = emoji.skins;
nonDefaultSkins.forEach(skin => {
const [matchingRawCodePoints,matchingRawEmoji] = Object.entries(emojiFromRawData.skin_variations).find((pair) => {
const [, value] = pair;
return value.unified.toLowerCase() === skin.unified;
});
if (matchingRawEmoji && matchingRawCodePoints) {
// At the time of writing, the json from `@emoji-mart/data` doesn't have data
// for emoji like `woman-heart-woman` with two different skin tones.
const skinToneCode = matchingRawCodePoints.split('-')[0];
skin_variations[skinToneCode] = {
unified: matchingRawEmoji.unified.toUpperCase(),
non_qualified: null,
sheet_x: matchingRawEmoji.sheet_x,
sheet_y: matchingRawEmoji.sheet_y,
has_img_twitter: true,
native: unifiedToNative(matchingRawEmoji.unified.toUpperCase())
};
}
});
}
return {
a: emoji.name,
b: unified,
c: undefined,
f: true,
j: [emoji.id, ...emoji.keywords],
k: [emojiFromRawData.sheet_x, emojiFromRawData.sheet_y],
m: emoji.emoticons?.[0],
l: emoji.emoticons,
o: emoji.version,
id: emoji.id,
skin_variations,
native: unifiedToNative(unified.toUpperCase())
};
}).compact().keyBy(e => e.id).mapValues(e => _.omit(e, 'id')).value()
};
if (data.compressed) {
emojiMartUncompress(data);
}
const emojiMartData = data;
@ -32,15 +105,10 @@ const shortcodeMap = {};
const shortCodesToEmojiData = {};
const emojisWithoutShortCodes = [];
Object.keys(emojiIndex.emojis).forEach(key => {
let emoji = emojiIndex.emojis[key];
Object.keys(emojiMart5Data.emojis).forEach(key => {
let emoji = emojiMart5Data.emojis[key];
// Emojis with skin tone modifiers are stored like this
if (Object.hasOwn(emoji, '1')) {
emoji = emoji['1'];
}
shortcodeMap[emoji.native] = emoji.id;
shortcodeMap[emoji.skins[0].native] = emoji.id;
});
const stripModifiers = unicode => {
@ -84,13 +152,9 @@ Object.keys(emojiMap).forEach(key => {
}
});
Object.keys(emojiIndex.emojis).forEach(key => {
let emoji = emojiIndex.emojis[key];
Object.keys(emojiMartData.emojis).forEach(key => {
let emoji = emojiMartData.emojis[key];
// Emojis with skin tone modifiers are stored like this
if (Object.hasOwn(emoji, '1')) {
emoji = emoji['1'];
}
const { native } = emoji;
let { short_names, search, unified } = emojiMartData.emojis[key];
@ -135,4 +199,5 @@ module.exports = JSON.parse(JSON.stringify([
emojiMartData.categories,
emojiMartData.aliases,
emojisWithoutShortCodes,
emojiMartData
]));

View file

@ -8,14 +8,15 @@ import type { Search, ShortCodesToEmojiData } from './emoji_compressed';
import emojiCompressed from './emoji_compressed';
import { unicodeToUnifiedName } from './unicode_to_unified_name';
type Emojis = {
[key in NonNullable<keyof ShortCodesToEmojiData>]: {
type Emojis = Record<
NonNullable<keyof ShortCodesToEmojiData>,
{
native: BaseEmoji['native'];
search: Search;
short_names: Emoji['short_names'];
unified: Emoji['unified'];
};
};
}
>;
const [
shortCodesToEmojiData,

View file

@ -1,5 +1,5 @@
import Emoji from 'emoji-mart/dist-es/components/emoji/emoji';
import Picker from 'emoji-mart/dist-es/components/picker/picker';
import Emoji from 'emoji-mart/dist-es/components/emoji/nimble-emoji';
import Picker from 'emoji-mart/dist-es/components/picker/nimble-picker';
export {
Picker,

File diff suppressed because one or more lines are too long

View file

@ -9,12 +9,13 @@ import type {
import emojiCompressed from './emoji_compressed';
import { unicodeToFilename } from './unicode_to_filename';
type UnicodeMapping = {
[key in FilenameData[number][0]]: {
type UnicodeMapping = Record<
FilenameData[number][0],
{
shortCode: ShortCodesToEmojiDataKey;
filename: FilenameData[number][number];
};
};
}
>;
const [
shortCodesToEmojiData,

View file

@ -17,7 +17,7 @@ export const ColumnSettings: React.FC = () => {
const dispatch = useAppDispatch();
const onChange = useCallback(
(key: string, checked: boolean) => {
(key: string[], checked: boolean) => {
dispatch(changeSetting(['home', ...key], checked));
},
[dispatch],

View file

@ -4,7 +4,6 @@ import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
import { Helmet } from 'react-helmet';
import { useSearchParam } from '@/hooks/useSearchParam';
import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
@ -20,6 +19,7 @@ import { Icon } from 'mastodon/components/icon';
import ScrollableList from 'mastodon/components/scrollable_list';
import Status from 'mastodon/containers/status_container';
import { Search } from 'mastodon/features/compose/components/search';
import { useSearchParam } from 'mastodon/hooks/useSearchParam';
import type { Hashtag as HashtagType } from 'mastodon/models/tags';
import { useAppDispatch, useAppSelector } from 'mastodon/store';

View file

@ -6,11 +6,11 @@ import { useEffect, useCallback } from 'react';
import { Provider } from 'react-redux';
import { useRenderSignal } from 'mastodon/../hooks/useRenderSignal';
import { fetchStatus, toggleStatusSpoilers } from 'mastodon/actions/statuses';
import { hydrateStore } from 'mastodon/actions/store';
import { Router } from 'mastodon/components/router';
import { DetailedStatus } from 'mastodon/features/status/components/detailed_status';
import { useRenderSignal } from 'mastodon/hooks/useRenderSignal';
import initialState from 'mastodon/initial_state';
import { IntlProvider } from 'mastodon/locales';
import { makeGetStatus, makeGetPictureInPicture } from 'mastodon/selectors';

View file

@ -221,12 +221,12 @@ export const DetailedStatus: React.FC<{
/>
);
}
} else if (status.get('spoiler_text').length === 0) {
} else if (status.get('card')) {
media = (
<Card
sensitive={status.get('sensitive')}
onOpenMedia={onOpenMedia}
card={status.get('card', null)}
card={status.get('card')}
/>
);
}

View file

@ -8,26 +8,31 @@ import {
} from 'react-intl';
import { Helmet } from 'react-helmet';
import { Link, useParams } from 'react-router-dom';
import { apiGetTermsOfService } from 'mastodon/api/instance';
import type { ApiTermsOfServiceJSON } from 'mastodon/api_types/instance';
import { Column } from 'mastodon/components/column';
import { Skeleton } from 'mastodon/components/skeleton';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
const messages = defineMessages({
title: { id: 'terms_of_service.title', defaultMessage: 'Terms of Service' },
});
interface Params {
date?: string;
}
const TermsOfService: React.FC<{
multiColumn: boolean;
}> = ({ multiColumn }) => {
const intl = useIntl();
const { date } = useParams<Params>();
const [response, setResponse] = useState<ApiTermsOfServiceJSON>();
const [loading, setLoading] = useState(true);
useEffect(() => {
apiGetTermsOfService()
apiGetTermsOfService(date)
.then((data) => {
setResponse(data);
setLoading(false);
@ -36,7 +41,7 @@ const TermsOfService: React.FC<{
.catch(() => {
setLoading(false);
});
}, []);
}, [date]);
if (!loading && !response) {
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
@ -55,23 +60,60 @@ const TermsOfService: React.FC<{
defaultMessage='Terms of Service'
/>
</h3>
<p>
<FormattedMessage
id='privacy_policy.last_updated'
defaultMessage='Last updated {date}'
values={{
date: loading ? (
<Skeleton width='10ch' />
) : (
<FormattedDate
value={response?.updated_at}
year='numeric'
month='short'
day='2-digit'
<p className='prose'>
{response?.effective ? (
<FormattedMessage
id='privacy_policy.last_updated'
defaultMessage='Last updated {date}'
values={{
date: (
<FormattedDate
value={response.effective_date}
year='numeric'
month='short'
day='2-digit'
/>
),
}}
/>
) : (
<FormattedMessage
id='terms_of_service.effective_as_of'
defaultMessage='Effective as of {date}'
values={{
date: (
<FormattedDate
value={response?.effective_date}
year='numeric'
month='short'
day='2-digit'
/>
),
}}
/>
)}
{response?.succeeded_by && (
<>
{' · '}
<Link to={`/terms-of-service/${response.succeeded_by}`}>
<FormattedMessage
id='terms_of_service.upcoming_changes_on'
defaultMessage='Upcoming changes on {date}'
values={{
date: (
<FormattedDate
value={response.succeeded_by}
year='numeric'
month='short'
day='2-digit'
/>
),
}}
/>
),
}}
/>
</Link>
</>
)}
</p>
</div>

View file

@ -101,6 +101,7 @@ const EmbedModal: React.FC<{
/>
<iframe
// eslint-disable-next-line @typescript-eslint/no-deprecated
frameBorder='0'
ref={iframeRef}
sandbox='allow-scripts allow-same-origin'

View file

@ -205,7 +205,7 @@ class SwitchingColumnsArea extends PureComponent {
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
<WrappedRoute path='/about' component={About} content={children} />
<WrappedRoute path='/privacy-policy' component={PrivacyPolicy} content={children} />
<WrappedRoute path='/terms-of-service' component={TermsOfService} content={children} />
<WrappedRoute path='/terms-of-service/:date?' component={TermsOfService} content={children} />
<WrappedRoute path={['/home', '/timelines/home']} component={HomeTimeline} content={children} />
<Redirect from='/timelines/public' to='/public' exact />

View file

@ -697,6 +697,7 @@
"poll_button.remove_poll": "Премахване на анкета",
"privacy.change": "Промяна на поверителността на публикация",
"privacy.direct.long": "Споменатите в публикацията",
"privacy.direct.short": "Частно споменаване",
"privacy.private.long": "Само последователите ви",
"privacy.private.short": "Последователи",
"privacy.public.long": "Всеки във и извън Mastodon",
@ -871,7 +872,9 @@
"subscribed_languages.target": "Промяна на абонираните езици за {target}",
"tabs_bar.home": "Начало",
"tabs_bar.notifications": "Известия",
"terms_of_service.effective_as_of": "В сила от {date}",
"terms_of_service.title": "Условия на услугата",
"terms_of_service.upcoming_changes_on": "Предстоящи промени на {date}",
"time_remaining.days": "{number, plural, one {остава # ден} other {остават # дни}}",
"time_remaining.hours": "{number, plural, one {остава # час} other {остават # часа}}",
"time_remaining.minutes": "{number, plural, one {остава # минута} other {остават # минути}}",

View file

@ -871,7 +871,9 @@
"subscribed_languages.target": "Canvia les llengües subscrites per a {target}",
"tabs_bar.home": "Inici",
"tabs_bar.notifications": "Notificacions",
"terms_of_service.effective_as_of": "En vigor a partir de {date}",
"terms_of_service.title": "Condicions de servei",
"terms_of_service.upcoming_changes_on": "Propers canvis el {date}",
"time_remaining.days": "{number, plural, one {# dia restant} other {# dies restants}}",
"time_remaining.hours": "{number, plural, one {# hora restant} other {# hores restants}}",
"time_remaining.minutes": "{number, plural, one {# minut restant} other {# minuts restants}}",

View file

@ -86,10 +86,10 @@
"alert.unexpected.message": "Objevila se neočekávaná chyba.",
"alert.unexpected.title": "Jejda!",
"alt_text_badge.title": "Popisek",
"alt_text_modal.add_alt_text": "Přidat alt text",
"alt_text_modal.add_alt_text": "Přidat popisek",
"alt_text_modal.add_text_from_image": "Přidat text z obrázku",
"alt_text_modal.cancel": "Zrušit",
"alt_text_modal.change_thumbnail": "Změnit miniaturu",
"alt_text_modal.change_thumbnail": "Změnit náhled",
"alt_text_modal.describe_for_people_with_hearing_impairments": "Popište to pro osoby se sluchovým postižením…",
"alt_text_modal.describe_for_people_with_visual_impairments": "Popište to pro osoby se zrakovým postižením…",
"alt_text_modal.done": "Hotovo",
@ -111,7 +111,7 @@
"annual_report.summary.most_used_hashtag.none": "Žádné",
"annual_report.summary.new_posts.new_posts": "nové příspěvky",
"annual_report.summary.percentile.text": "<topLabel>To vás umisťuje do vrcholu</topLabel><percentage></percentage><bottomLabel>{domain} uživatelů.</bottomLabel>",
"annual_report.summary.percentile.we_wont_tell_bernie": "To, že jste zdejší smetánka zůstane mezi námi ;).",
"annual_report.summary.percentile.we_wont_tell_bernie": "To, že jste zdejší smetánka, zůstane mezi námi ;).",
"annual_report.summary.thanks": "Děkujeme, že jste součástí Mastodonu!",
"attachments_list.unprocessed": "(nezpracováno)",
"audio.hide": "Skrýt zvuk",
@ -218,10 +218,10 @@
"confirmations.logout.confirm": "Odhlásit se",
"confirmations.logout.message": "Opravdu se chcete odhlásit?",
"confirmations.logout.title": "Odhlásit se?",
"confirmations.missing_alt_text.confirm": "Přidat alt text",
"confirmations.missing_alt_text.message": "Váš příspěvek obsahuje média bez alt textu. Přidání popisů pomáhá zpřístupnit váš obsah většímu počtu lidí.",
"confirmations.missing_alt_text.confirm": "Přidat popisek",
"confirmations.missing_alt_text.message": "Váš příspěvek obsahuje média bez popisku. Jejich přidání pomáhá zpřístupnit váš obsah většímu počtu lidí.",
"confirmations.missing_alt_text.secondary": "Přesto odeslat",
"confirmations.missing_alt_text.title": "Přidat alt text?",
"confirmations.missing_alt_text.title": "Přidat popisek?",
"confirmations.mute.confirm": "Skrýt",
"confirmations.redraft.confirm": "Smazat a přepsat",
"confirmations.redraft.message": "Jste si jistí, že chcete odstranit tento příspěvek a vytvořit z něj koncept? Oblíbené a boosty budou ztraceny a odpovědi na původní příspěvek ztratí kontext.",
@ -261,18 +261,18 @@
"domain_block_modal.they_wont_know": "Nebude vědět, že je zablokován*a.",
"domain_block_modal.title": "Blokovat doménu?",
"domain_block_modal.you_will_lose_num_followers": "Ztratíte {followersCount, plural, one {{followersCountDisplay} sledujícího} few {{followersCountDisplay} sledující} many {{followersCountDisplay} sledujících} other {{followersCountDisplay} sledujících}} a {followingCount, plural, one {{followingCountDisplay} sledovaného} few {{followingCountDisplay} sledované} many {{followingCountDisplay} sledovaných} other {{followingCountDisplay} sledovaných}}.",
"domain_block_modal.you_will_lose_relationships": "Ztratíte všechny sledující a lidi, které sledujete z tohoto serveru.",
"domain_block_modal.you_will_lose_relationships": "Ztratíte všechny sledující a sledované z tohoto serveru.",
"domain_block_modal.you_wont_see_posts": "Neuvidíte příspěvky ani upozornění od uživatelů z tohoto serveru.",
"domain_pill.activitypub_lets_connect": "Umožňuje vám spojit se a komunikovat s lidmi nejen na Mastodonu, ale i s dalšími sociálními aplikacemi.",
"domain_pill.activitypub_lets_connect": "Umožňuje vám spojit se a komunikovat s lidmi nejen na Mastodonu, ale i na jiných sociálních aplikacích.",
"domain_pill.activitypub_like_language": "ActivityPub je jako jazyk, kterým Mastodon mluví s jinými sociálními sítěmi.",
"domain_pill.server": "Server",
"domain_pill.their_handle": "Handle:",
"domain_pill.their_server": "Jejich digitální domov, kde žijí jejich všechny příspěvky.",
"domain_pill.their_username": "Jejich jedinečný identikátor na jejich serveru. Je možné najít uživatele se stejným uživatelským jménem na jiných serverech.",
"domain_pill.their_username": "Jejich jedinečný identifikátor na jejich serveru. Je možné, že na jiných serverech jsou uživatelé se stejným uživatelským jménem.",
"domain_pill.username": "Uživatelské jméno",
"domain_pill.whats_in_a_handle": "Co obsahuje handle?",
"domain_pill.who_they_are": "Protože handle říkají kdo je kdo a také kde, je možné interagovat s lidmi napříč sociálními weby <button>platforem postavených na ActivityPub</button>.",
"domain_pill.who_you_are": "Protože handle říká kdo jsi a kde jsi, mohou s tebou lidé komunikovat napříč sociálními weby <button>platforem postavených na ActivityPub</button>.",
"domain_pill.who_they_are": "Protože handly říkají kdo je kdo a také kde, je možné interagovat s lidmi napříč sociálním webem, skládajícím se z <button>platforem postavených na ActivityPub</button>.",
"domain_pill.who_you_are": "Protože handle říká kdo jsi a kde jsi, mohou s tebou komunikovat lidé napříč sociálním webem, skládajícím se z <button>platforem postavených na ActivityPub</button>.",
"domain_pill.your_handle": "Tvůj handle:",
"domain_pill.your_server": "Tvůj digitální domov, kde žijí všechny tvé příspěvky. Nelíbí se ti? Kdykoliv se přesuň na jiný server a vezmi si sebou i své sledující.",
"domain_pill.your_username": "Tvůj jedinečný identifikátor na tomto serveru. Je možné najít uživatele se stejným uživatelským jménem na jiných serverech.",
@ -293,7 +293,7 @@
"emoji_button.search_results": "Výsledky hledání",
"emoji_button.symbols": "Symboly",
"emoji_button.travel": "Cestování a místa",
"empty_column.account_hides_collections": "Tento uživatel se rozhodl nezveřejňovat tuto informaci",
"empty_column.account_hides_collections": "Tento uživatel se rozhodl tuto informaci nezveřejňovat",
"empty_column.account_suspended": "Účet je pozastaven",
"empty_column.account_timeline": "Nejsou tu žádné příspěvky!",
"empty_column.account_unavailable": "Profil není dostupný",
@ -574,7 +574,7 @@
"notification.mention": "Zmínka",
"notification.mentioned_you": "{name} vás zmínil",
"notification.moderation-warning.learn_more": "Zjistit více",
"notification.moderation_warning": "Obdrželi jste moderační varování",
"notification.moderation_warning": "Obdrželi jste varování od moderátorů",
"notification.moderation_warning.action_delete_statuses": "Některé z vašich příspěvků byly odstraněny.",
"notification.moderation_warning.action_disable": "Váš účet je zablokován.",
"notification.moderation_warning.action_mark_statuses_as_sensitive": "Některé z vašich příspěvků byly označeny jako citlivé.",
@ -872,7 +872,9 @@
"subscribed_languages.target": "Změnit odebírané jazyky na {target}",
"tabs_bar.home": "Domů",
"tabs_bar.notifications": "Oznámení",
"terms_of_service.effective_as_of": "Platné od {date}",
"terms_of_service.title": "Podmínky služby",
"terms_of_service.upcoming_changes_on": "Nadcházející změny v {date}",
"time_remaining.days": "{number, plural, one {Zbývá # den} few {Zbývají # dny} many {Zbývá # dní} other {Zbývá # dní}}",
"time_remaining.hours": "{number, plural, one {Zbývá # hodina} few {Zbývají # hodiny} many {Zbývá # hodin} other {Zbývá # hodin}}",
"time_remaining.minutes": "{number, plural, one {Zbývá # minuta} few {Zbývají # minuty} many {Zbývá # minut} other {Zbývá # minut}}",

View file

@ -562,6 +562,7 @@
"notification.favourite": "Ffafriodd {name} eich postiad",
"notification.favourite.name_and_others_with_link": "Ffafriodd {name} a <a>{count, plural, one {# arall} other {# arall}}</a> eich postiad",
"notification.favourite_pm": "Mae {name} wedi ffefrynnu eich cyfeiriad preifat",
"notification.favourite_pm.name_and_others_with_link": "Mae {name} a <a>{count, plural, one {# arall} other {# arall}}</a> wedi hoffi'ch crybwylliad preifat",
"notification.follow": "Dilynodd {name} chi",
"notification.follow.name_and_others": "Mae {name} a <a>{count, plural, zero {}one {# arall} two {# arall} few {# arall} many {# others} other {# arall}}</a> nawr yn eich dilyn chi",
"notification.follow_request": "Mae {name} wedi gwneud cais i'ch dilyn",
@ -696,6 +697,7 @@
"poll_button.remove_poll": "Tynnu pleidlais",
"privacy.change": "Addasu preifatrwdd y post",
"privacy.direct.long": "Pawb sydd â sôn amdanyn nhw yn y postiad",
"privacy.direct.short": "Crybwylliad preifat",
"privacy.private.long": "Eich dilynwyr yn unig",
"privacy.private.short": "Dilynwyr",
"privacy.public.long": "Unrhyw ar ac oddi ar Mastodon",
@ -870,7 +872,9 @@
"subscribed_languages.target": "Newid ieithoedd tanysgrifio {target}",
"tabs_bar.home": "Cartref",
"tabs_bar.notifications": "Hysbysiadau",
"terms_of_service.effective_as_of": "Yn effeithiol ers {date}",
"terms_of_service.title": "Telerau Gwasanaeth",
"terms_of_service.upcoming_changes_on": "Newidiadau i ddod ar {date}",
"time_remaining.days": "{number, plural, one {# diwrnod} other {# diwrnod}} ar ôl",
"time_remaining.hours": "{number, plural, one {# awr} other {# awr}} ar ôl",
"time_remaining.minutes": "{number, plural, one {# munud} other {# munud}} ar ôl",

View file

@ -153,7 +153,7 @@
"column.domain_blocks": "Blokerede domæner",
"column.edit_list": "Redigér liste",
"column.favourites": "Favoritter",
"column.firehose": "Realtids-strømme",
"column.firehose": "Aktuelt",
"column.follow_requests": "Følgeanmodninger",
"column.home": "Hjem",
"column.list_members": "Håndtér listemedlemmer",
@ -872,7 +872,9 @@
"subscribed_languages.target": "Skift abonnementssprog for {target}",
"tabs_bar.home": "Hjem",
"tabs_bar.notifications": "Notifikationer",
"terms_of_service.effective_as_of": "Gældende pr. {date}",
"terms_of_service.title": "Tjenestevilkår",
"terms_of_service.upcoming_changes_on": "Kommende ændringer pr. {date}",
"time_remaining.days": "{number, plural, one {# dag} other {# dage}} tilbage",
"time_remaining.hours": "{number, plural, one {# time} other {# timer}} tilbage",
"time_remaining.minutes": "{number, plural, one {# minut} other {# minutter}} tilbage",

View file

@ -872,7 +872,9 @@
"subscribed_languages.target": "Abonnierte Sprachen für {target} ändern",
"tabs_bar.home": "Startseite",
"tabs_bar.notifications": "Benachrichtigungen",
"terms_of_service.effective_as_of": "Gültig ab {date}",
"terms_of_service.title": "Nutzungsbedingungen",
"terms_of_service.upcoming_changes_on": "Anstehende Änderungen am {date}",
"time_remaining.days": "noch {number, plural, one {# Tag} other {# Tage}}",
"time_remaining.hours": "noch {number, plural, one {# Stunde} other {# Stunden}}",
"time_remaining.minutes": "noch {number, plural, one {# Minute} other {# Minuten}}",

View file

@ -872,7 +872,9 @@
"subscribed_languages.target": "Change subscribed languages for {target}",
"tabs_bar.home": "Home",
"tabs_bar.notifications": "Notifications",
"terms_of_service.effective_as_of": "Effective as of {date}",
"terms_of_service.title": "Terms of Service",
"terms_of_service.upcoming_changes_on": "Upcoming changes on {date}",
"time_remaining.days": "{number, plural, one {# day} other {# days}} left",
"time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
"time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",

View file

@ -872,7 +872,9 @@
"subscribed_languages.target": "Ŝanĝu abonitajn lingvojn por {target}",
"tabs_bar.home": "Hejmo",
"tabs_bar.notifications": "Sciigoj",
"terms_of_service.effective_as_of": "Ĝi ekvalidas de {date}",
"terms_of_service.title": "Kondiĉoj de uzado",
"terms_of_service.upcoming_changes_on": "Venontaj ŝanĝoj el {date}",
"time_remaining.days": "{number, plural, one {# tago} other {# tagoj}} restas",
"time_remaining.hours": "{number, plural, one {# horo} other {# horoj}} restas",
"time_remaining.minutes": "{number, plural, one {# minuto} other {# minutoj}} restas",

View file

@ -872,7 +872,9 @@
"subscribed_languages.target": "Cambiar idiomas suscritos para {target}",
"tabs_bar.home": "Principal",
"tabs_bar.notifications": "Notificaciones",
"terms_of_service.effective_as_of": "Efectivo a partir de {date}",
"terms_of_service.title": "Términos del servicio",
"terms_of_service.upcoming_changes_on": "Próximos cambios el {date}",
"time_remaining.days": "{number, plural,one {queda # día} other {quedan # días}}",
"time_remaining.hours": "{number, plural,one {queda # hora} other {quedan # horas}}",
"time_remaining.minutes": "{number, plural,one {queda # minuto} other {quedan # minutos}}",

View file

@ -195,7 +195,7 @@
"compose_form.publish": "Publicar",
"compose_form.publish_form": "Nueva publicación",
"compose_form.reply": "Respuesta",
"compose_form.save_changes": "Actualización",
"compose_form.save_changes": "Actualizar",
"compose_form.spoiler.marked": "Quitar advertencia de contenido",
"compose_form.spoiler.unmarked": "Añadir advertencia de contenido",
"compose_form.spoiler_placeholder": "Advertencia de contenido (opcional)",
@ -872,7 +872,9 @@
"subscribed_languages.target": "Cambiar idiomas suscritos para {target}",
"tabs_bar.home": "Inicio",
"tabs_bar.notifications": "Notificaciones",
"terms_of_service.effective_as_of": "En vigor a partir del {date}",
"terms_of_service.title": "Condiciones del servicio",
"terms_of_service.upcoming_changes_on": "Próximos cambios el {date}",
"time_remaining.days": "{number, plural, one {# día restante} other {# días restantes}}",
"time_remaining.hours": "{number, plural, one {# hora restante} other {# horas restantes}}",
"time_remaining.minutes": "{number, plural, one {# minuto restante} other {# minutos restantes}}",

View file

@ -872,7 +872,9 @@
"subscribed_languages.target": "Cambiar idiomas suscritos para {target}",
"tabs_bar.home": "Inicio",
"tabs_bar.notifications": "Notificaciones",
"terms_of_service.effective_as_of": "En vigor a partir del {date}",
"terms_of_service.title": "Términos del servicio",
"terms_of_service.upcoming_changes_on": "Próximos cambios el {date}",
"time_remaining.days": "{number, plural, one {# día restante} other {# días restantes}}",
"time_remaining.hours": "{number, plural, one {# hora restante} other {# horas restantes}}",
"time_remaining.minutes": "{number, plural, one {# minuto restante} other {# minutos restantes}}",

View file

@ -218,6 +218,10 @@
"confirmations.logout.confirm": "خروج از حساب",
"confirmations.logout.message": "مطمئنید می‌خواهید خارج شوید؟",
"confirmations.logout.title": "خروج؟",
"confirmations.missing_alt_text.confirm": "متن جایگزین را اضافه کنید",
"confirmations.missing_alt_text.message": "پست شما حاوی رسانه بدون متن جایگزین است. افزودن توضیحات کمک می کند تا محتوای شما برای افراد بیشتری قابل دسترسی باشد.",
"confirmations.missing_alt_text.secondary": "به هر حال پست کن",
"confirmations.missing_alt_text.title": "متن جایگزین اضافه شود؟",
"confirmations.mute.confirm": "خموش",
"confirmations.redraft.confirm": "حذف و بازنویسی",
"confirmations.redraft.message": "مطمئنید که می‌خواهید این فرسته را حذف کنید و از نو بنویسید؟ با این کار تقویت‌ها و پسندهایش از دست رفته و پاسخ‌ها به آن بی‌مرجع می‌شود.",
@ -693,6 +697,7 @@
"poll_button.remove_poll": "برداشتن نظرسنجی",
"privacy.change": "تغییر محرمانگی فرسته",
"privacy.direct.long": "هرکسی که در فرسته نام برده شده",
"privacy.direct.short": "ذکر خصوصی",
"privacy.private.long": "تنها پی‌گیرندگانتان",
"privacy.private.short": "پی‌گیرندگان",
"privacy.public.long": "هرکسی در و بیرون از ماستودون",
@ -867,7 +872,9 @@
"subscribed_languages.target": "تغییر زبان‌های مشترک شده برای {target}",
"tabs_bar.home": "خانه",
"tabs_bar.notifications": "آگاهی‌ها",
"terms_of_service.effective_as_of": "اعمال شده از {date}",
"terms_of_service.title": "شرایط خدمات",
"terms_of_service.upcoming_changes_on": "تغییرات پیش رو در {date}",
"time_remaining.days": "{number, plural, one {# روز} other {# روز}} باقی مانده",
"time_remaining.hours": "{number, plural, one {# ساعت} other {# ساعت}} باقی مانده",
"time_remaining.minutes": "{number, plural, one {# دقیقه} other {# دقیقه}} باقی مانده",

View file

@ -625,7 +625,7 @@
"notifications.column_settings.follow_request": "Uudet seurantapyynnöt:",
"notifications.column_settings.group": "Ryhmitä",
"notifications.column_settings.mention": "Maininnat:",
"notifications.column_settings.poll": "Äänestyksen tulokset:",
"notifications.column_settings.poll": "Äänestystulokset:",
"notifications.column_settings.push": "Puskuilmoitukset",
"notifications.column_settings.reblog": "Tehostukset:",
"notifications.column_settings.show": "Näytä sarakkeessa",
@ -639,7 +639,7 @@
"notifications.filter.favourites": "Suosikit",
"notifications.filter.follows": "Seuraamiset",
"notifications.filter.mentions": "Maininnat",
"notifications.filter.polls": "Äänestyksen tulokset",
"notifications.filter.polls": "Äänestystulokset",
"notifications.filter.statuses": "Päivitykset seuraamiltasi käyttäjiltä",
"notifications.grant_permission": "Myönnä käyttöoikeus.",
"notifications.group": "{count} ilmoitusta",
@ -872,7 +872,9 @@
"subscribed_languages.target": "Vaihda tilattuja kieliä käyttäjältä {target}",
"tabs_bar.home": "Koti",
"tabs_bar.notifications": "Ilmoitukset",
"terms_of_service.effective_as_of": "Tulee voimaan {date}",
"terms_of_service.title": "Käyttöehdot",
"terms_of_service.upcoming_changes_on": "Tulevia muutoksia {date}",
"time_remaining.days": "{number, plural, one {# päivä} other {# päivää}} jäljellä",
"time_remaining.hours": "{number, plural, one {# tunti} other {# tuntia}} jäljellä",
"time_remaining.minutes": "{number, plural, one {# minuutti} other {# minuuttia}} jäljellä",

View file

@ -872,7 +872,9 @@
"subscribed_languages.target": "Broyt haldaramál fyri {target}",
"tabs_bar.home": "Heim",
"tabs_bar.notifications": "Fráboðanir",
"terms_of_service.effective_as_of": "Galdandi frá {date}",
"terms_of_service.title": "Tænastutreytir",
"terms_of_service.upcoming_changes_on": "Komandi broytingar {date}",
"time_remaining.days": "{number, plural, one {# dagur} other {# dagar}} eftir",
"time_remaining.hours": "{number, plural, one {# tími} other {# tímar}} eftir",
"time_remaining.minutes": "{number, plural, one {# minuttur} other {# minuttir}} eftir",

View file

@ -872,7 +872,9 @@
"subscribed_languages.target": "Athraigh teangacha suibscríofa le haghaidh {target}",
"tabs_bar.home": "Baile",
"tabs_bar.notifications": "Fógraí",
"terms_of_service.effective_as_of": "I bhfeidhm ó {date}",
"terms_of_service.title": "Téarmaí Seirbhíse",
"terms_of_service.upcoming_changes_on": "Athruithe atá le teacht ar {date}",
"time_remaining.days": "{number, plural, one {# lá} other {# lá}} fágtha",
"time_remaining.hours": "{number, plural, one {# uair} other {# uair}} fágtha",
"time_remaining.minutes": "{number, plural, one {# nóiméad} other {# nóiméad}} fágtha",

View file

@ -872,7 +872,9 @@
"subscribed_languages.target": "Cambiar a subscrición a idiomas para {target}",
"tabs_bar.home": "Inicio",
"tabs_bar.notifications": "Notificacións",
"terms_of_service.effective_as_of": "Con efecto desde o {date}",
"terms_of_service.title": "Termos do Servizo",
"terms_of_service.upcoming_changes_on": "Cambios por vir o {date}",
"time_remaining.days": "Remata en {number, plural, one {# día} other {# días}}",
"time_remaining.hours": "Remata en {number, plural, one {# hora} other {# horas}}",
"time_remaining.minutes": "Remata en {number, plural, one {# minuto} other {# minutos}}",

View file

@ -872,7 +872,9 @@
"subscribed_languages.target": "שינוי רישום שפה עבור {target}",
"tabs_bar.home": "פיד הבית",
"tabs_bar.notifications": "התראות",
"terms_of_service.effective_as_of": "בתוקף החל מתאריך {date}",
"terms_of_service.title": "תנאי השירות",
"terms_of_service.upcoming_changes_on": "שינויים עתידיים שיחולו ביום {date}",
"time_remaining.days": "נותרו {number, plural, one {# יום} other {# ימים}}",
"time_remaining.hours": "נותרו {number, plural, one {# שעה} other {# שעות}}",
"time_remaining.minutes": "נותרו {number, plural, one {# דקה} other {# דקות}}",

View file

@ -872,7 +872,9 @@
"subscribed_languages.target": "Feliratkozott nyelvek módosítása {target} esetében",
"tabs_bar.home": "Kezdőlap",
"tabs_bar.notifications": "Értesítések",
"terms_of_service.effective_as_of": "Hatálybalépés dátuma: {date}",
"terms_of_service.title": "Felhasználási feltételek",
"terms_of_service.upcoming_changes_on": "Érkező változások: {date}",
"time_remaining.days": "{number, plural, one {# nap} other {# nap}} van hátra",
"time_remaining.hours": "{number, plural, one {# óra} other {# óra}} van hátra",
"time_remaining.minutes": "{number, plural, one {# perc} other {# perc}} van hátra",

View file

@ -306,7 +306,7 @@
"empty_column.favourited_statuses": "Tu non ha alcun message favorite ancora. Quando tu marca un message como favorite, illo apparera hic.",
"empty_column.favourites": "Necuno ha ancora marcate iste message como favorite. Quando alcuno lo face, ille apparera hic.",
"empty_column.follow_requests": "Tu non ha ancora requestas de sequimento. Quando tu recipe un, illo apparera hic.",
"empty_column.followed_tags": "Tu non ha ancora sequite alcun hashtags. Quando tu lo face, illos apparera hic.",
"empty_column.followed_tags": "Tu non seque ancora alcun hashtags. Quando tu lo face, illos apparera hic.",
"empty_column.hashtag": "Il non ha ancora alcun cosa in iste hashtag.",
"empty_column.home": "Tu chronologia de initio es vacue! Seque plus personas pro plenar lo.",
"empty_column.list": "Iste lista es ancora vacue. Quando le membros de iste lista publica nove messages, illos apparera hic.",
@ -358,11 +358,11 @@
"follow_suggestions.hints.friends_of_friends": "Iste profilo es popular inter le gente que tu seque.",
"follow_suggestions.hints.most_followed": "Iste profilo es un del plus sequites sur {domain}.",
"follow_suggestions.hints.most_interactions": "Iste profilo ha recentemente recipite multe attention sur {domain}.",
"follow_suggestions.hints.similar_to_recently_followed": "Iste profilo es similar al profilos que tu ha recentemente sequite.",
"follow_suggestions.hints.similar_to_recently_followed": "Iste profilo es similar al profilos que tu ha recentemente comenciate a sequer.",
"follow_suggestions.personalized_suggestion": "Suggestion personalisate",
"follow_suggestions.popular_suggestion": "Suggestion personalisate",
"follow_suggestions.popular_suggestion_longer": "Popular sur {domain}",
"follow_suggestions.similar_to_recently_followed_longer": "Similar al profilos que tu ha sequite recentemente",
"follow_suggestions.similar_to_recently_followed_longer": "Similar al profilos que tu ha recentemente comenciate a sequer",
"follow_suggestions.view_all": "Vider toto",
"follow_suggestions.who_to_follow": "Qui sequer",
"followed_tags": "Hashtags sequite",
@ -563,8 +563,8 @@
"notification.favourite.name_and_others_with_link": "{name} e <a>{count, plural, one {# altere} other {# alteres}}</a> ha marcate tu message como favorite",
"notification.favourite_pm": "{name} ha marcate tu mention privte como favorite",
"notification.favourite_pm.name_and_others_with_link": "{name} e <a>{count, plural, one {# altere} other {# alteres}}</a> ha marcate tu mention private como favorite",
"notification.follow": "{name} te ha sequite",
"notification.follow.name_and_others": "{name} e <a>{count, plural, one {# other} other {# alteres}}</a> te ha sequite",
"notification.follow": "{name} te seque",
"notification.follow.name_and_others": "{name} e <a>{count, plural, one {# other} other {# alteres}}</a> te seque",
"notification.follow_request": "{name} ha requestate de sequer te",
"notification.follow_request.name_and_others": "{name} e {count, plural, one {# altere} other {# alteres}} ha demandate de sequer te",
"notification.label.mention": "Mention",
@ -657,7 +657,7 @@
"notifications.policy.filter_limited_accounts_title": "Contos moderate",
"notifications.policy.filter_new_accounts.hint": "Create in le ultime {days, plural, one {die} other {# dies}}",
"notifications.policy.filter_new_accounts_title": "Nove contos",
"notifications.policy.filter_not_followers_hint": "Includente le personas que te ha sequite durante minus de {days, plural, one {un die} other {# dies}}",
"notifications.policy.filter_not_followers_hint": "Includente le personas que te seque desde minus de {days, plural, one {un die} other {# dies}}",
"notifications.policy.filter_not_followers_title": "Personas qui non te seque",
"notifications.policy.filter_not_following_hint": "Usque tu les approba manualmente",
"notifications.policy.filter_not_following_title": "Personas que tu non seque",

View file

@ -4,7 +4,7 @@
"about.disclaimer": "Mastodon esas libera, publikfonta e komercmarko di Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "Expliko nedisponebla",
"about.domain_blocks.preamble": "Mastodon generale permisas on vidar kontenajo e interagar kun uzanti de irga altra servilo en fediverso. Existas eceptioni quo facesis che ca partikulara servilo.",
"about.domain_blocks.silenced.explanation": "On generale ne vidar profili e kontenajo de ca servilo, se on ne reale trovar o voluntale juntar per sequar.",
"about.domain_blocks.silenced.explanation": "On generale ne vidar profili e enhavajo de ca servilo, se on ne intence serchar o voleskar per sequar.",
"about.domain_blocks.silenced.title": "Limitizita",
"about.domain_blocks.suspended.explanation": "Nula informi de ca servili procedagesos o retenesos o interchanjesos, do irga interago o komuniko kun uzanti de ca servili esas neposibla.",
"about.domain_blocks.suspended.title": "Restriktita",
@ -12,7 +12,7 @@
"about.powered_by": "Necentraligita sociala ret quo povigesas da {mastodon}",
"about.rules": "Servilreguli",
"account.account_note_header": "Personala noto",
"account.add_or_remove_from_list": "Insertez o removez de listi",
"account.add_or_remove_from_list": "Adjuntar o forigar de listi",
"account.badges.bot": "Boto",
"account.badges.group": "Grupo",
"account.block": "Blokusar @{name}",
@ -24,12 +24,12 @@
"account.direct": "Private mencionez @{name}",
"account.disable_notifications": "Cesez avizar me kande @{name} postas",
"account.domain_blocked": "Domain hidden",
"account.edit_profile": "Modifikar profilo",
"account.edit_profile": "Redaktar profilo",
"account.enable_notifications": "Avizez me kande @{name} postas",
"account.endorse": "Traito di profilo",
"account.featured_tags.last_status_at": "Antea posto ye {date}",
"account.featured_tags.last_status_never": "Nula posti",
"account.featured_tags.title": "Estalita hashtagi di {name}",
"account.featured_tags.title": "Ekstaca gretvorti di {name}",
"account.follow": "Sequar",
"account.follow_back": "Anke sequez",
"account.followers": "Sequanti",
@ -45,7 +45,7 @@
"account.languages": "Chanjez abonita lingui",
"account.link_verified_on": "Proprieteso di ca ligilo kontrolesis ye {date}",
"account.locked_info": "La privatesostaco di ca konto fixesas quale lokata. Proprietato manue kontrolas personi qui povas sequar.",
"account.media": "Medio",
"account.media": "Audvidaji",
"account.mention": "Mencionar @{name}",
"account.moved_to": "{name} indikis ke lua nova konto es nune:",
"account.mute": "Celar @{name}",
@ -56,7 +56,7 @@
"account.no_bio": "Deskriptajo ne provizesis.",
"account.open_original_page": "Apertez originala pagino",
"account.posts": "Mesaji",
"account.posts_with_replies": "Posti e respondi",
"account.posts_with_replies": "Afishi e respondi",
"account.report": "Denuncar @{name}",
"account.requested": "Vartante aprobo",
"account.requested_follow": "{name} demandis sequar tu",
@ -86,12 +86,38 @@
"alert.unexpected.message": "Neexpektita eroro eventis.",
"alert.unexpected.title": "Problemo!",
"alt_text_badge.title": "Alternativa texto",
"alt_text_modal.add_alt_text": "Adjuntar alternativa texto",
"alt_text_modal.add_text_from_image": "Adjuntar texto de imajo",
"alt_text_modal.cancel": "Nuligar",
"alt_text_modal.change_thumbnail": "Chanjar imajeto",
"alt_text_modal.describe_for_people_with_hearing_impairments": "Priskribar co por personi kun auddeskapableso…",
"alt_text_modal.describe_for_people_with_visual_impairments": "Priskribar co por personi kun viddeskapableso…",
"alt_text_modal.done": "Finis",
"announcement.announcement": "Anunco",
"annual_report.summary.archetype.booster": "La plurrepetanto",
"annual_report.summary.archetype.lurker": "La plurcelanto",
"annual_report.summary.archetype.oracle": "La pluraktivo",
"annual_report.summary.archetype.pollster": "La votinquestoiganto",
"annual_report.summary.archetype.replier": "La plurrespondanto",
"annual_report.summary.followers.followers": "sequanti",
"annual_report.summary.followers.total": "{count} sumo",
"annual_report.summary.here_it_is": "Caibe es vua rivido ye {year}:",
"annual_report.summary.highlighted_post.by_favourites": "maxim prizita mesajo",
"annual_report.summary.highlighted_post.by_reblogs": "maxim repetita mesajo",
"annual_report.summary.highlighted_post.by_replies": "mesajo kun la maxim multa respondi",
"annual_report.summary.highlighted_post.possessive": "di {name}",
"annual_report.summary.most_used_app.most_used_app": "maxim uzita aplikajo",
"annual_report.summary.most_used_hashtag.most_used_hashtag": "maxim uzita gretvorto",
"annual_report.summary.most_used_hashtag.none": "Nulo",
"annual_report.summary.new_posts.new_posts": "nova afishi",
"annual_report.summary.percentile.text": "<topLabel>To pozas vu sur la supro </topLabel><percentage></percentage><bottomLabel>di uzanti di {domain}.</bottomLabel>",
"annual_report.summary.percentile.we_wont_tell_bernie": "Ni ne dicas ad Bernio.",
"annual_report.summary.thanks": "Danki por partoprenar sur Mastodon!",
"attachments_list.unprocessed": "(neprocedita)",
"audio.hide": "Celez audio",
"block_modal.remote_users_caveat": "Ni questionos {domain} di la servilo por respektar vua decido. Publika posti forsan ankore estas videbla a neenirinta uzanti.",
"block_modal.show_less": "Montrar mine",
"block_modal.show_more": "Montrar plue",
"block_modal.show_more": "Montrar plu",
"block_modal.they_cant_mention": "Oli ne povas mencionar o sequar vu.",
"block_modal.they_cant_see_posts": "Oli ne povas vidar vua mesaji e vu ne vidos vidar olia.",
"block_modal.they_will_know": "Oli povas vidar ke oli esas blokusita.",
@ -110,6 +136,7 @@
"bundle_column_error.routing.body": "Demandita pagino ne povas trovesar. Ka vu certe ke URL en situobuxo esar korekta?",
"bundle_column_error.routing.title": "Eroro di 404",
"bundle_modal_error.close": "Klozez",
"bundle_modal_error.message": "Ulo nefuncionas dum chargar ca skreno.",
"bundle_modal_error.retry": "Probez itere",
"closed_registrations.other_server_instructions": "Nam Mastodon es descentraligita, on povas krear konto che altra servilo e senegarde interagar kun ca servilo.",
"closed_registrations_modal.description": "Nune on ne povas krear konto che {domain}, ma voluntez savar ke on ne bezonas konto specifike che {domain} por uzar Mastodon.",
@ -118,19 +145,22 @@
"closed_registrations_modal.title": "Krear konto che Mastodon",
"column.about": "Pri co",
"column.blocks": "Blokusita uzeri",
"column.bookmarks": "Libromarki",
"column.bookmarks": "Lektosigni",
"column.community": "Lokala tempolineo",
"column.create_list": "Krear listo",
"column.direct": "Privata mencioni",
"column.directory": "Videz profili",
"column.domain_blocks": "Hidden domains",
"column.domain_blocks": "Blokusita domeni",
"column.edit_list": "Redaktar listo",
"column.favourites": "Favoriziti",
"column.firehose": "Nuna flui",
"column.follow_requests": "Demandi di sequado",
"column.home": "Hemo",
"column.list_members": "Administrar listomembri",
"column.lists": "Listi",
"column.mutes": "Celita uzeri",
"column.notifications": "Savigi",
"column.pins": "Pinned toot",
"column.pins": "Adpinglita afishi",
"column.public": "Federata tempolineo",
"column_back_button.label": "Retro",
"column_header.hide_settings": "Celez ajusti",
@ -139,26 +169,28 @@
"column_header.pin": "Pinglagez",
"column_header.show_settings": "Montrez ajusti",
"column_header.unpin": "Depinglagez",
"column_search.cancel": "Nuligar",
"column_subheading.settings": "Ajusti",
"community.column_settings.local_only": "Lokala nur",
"community.column_settings.media_only": "Media only",
"community.column_settings.media_only": "Nur audvidaji",
"community.column_settings.remote_only": "Fora nur",
"compose.language.change": "Chanjez linguo",
"compose.language.search": "Trovez linguo...",
"compose.published.body": "Posto publikigita.",
"compose.published.open": "Apertez",
"compose.saved.body": "Posto konservita.",
"compose_form.direct_message_warning_learn_more": "Lernez pluse",
"compose_form.direct_message_warning_learn_more": "Lernez plu",
"compose_form.encryption_warning": "Posti en Mastodon ne intersequante chifrigesas. Ne partigez irga privata informo che Mastodon.",
"compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.",
"compose_form.hashtag_warning": "Ca afisho ne listigesos kun irga gretvorto pro ke ol ne es publika.",
"compose_form.lock_disclaimer": "Vua konto ne esas {locked}. Irgu povas sequar vu por vidar vua sequanto-nura posti.",
"compose_form.lock_disclaimer.lock": "klefagesas",
"compose_form.placeholder": "Quo esas en tua spirito?",
"compose_form.poll.duration": "Votpostoduro",
"compose_form.placeholder": "Quon vu pensas?",
"compose_form.poll.duration": "Votinquestoduro",
"compose_form.poll.multiple": "Multopla selekteso",
"compose_form.poll.option_placeholder": "Selektato {number}",
"compose_form.poll.switch_to_multiple": "Chanjez votposto por permisar multiselektaji",
"compose_form.poll.switch_to_single": "Chanjez votposto por permisar una selektajo",
"compose_form.poll.single": "Unopla selektato",
"compose_form.poll.switch_to_multiple": "Chanjar votinquesto por permisar multopla selektati",
"compose_form.poll.switch_to_single": "Chanjez votinquesto por permisar unopla selektato",
"compose_form.poll.type": "Stilo",
"compose_form.publish": "Posto",
"compose_form.publish_form": "Publish",
@ -175,14 +207,21 @@
"confirmations.delete_list.confirm": "Efacez",
"confirmations.delete_list.message": "Ka vu certe volas netempale efacar ca listo?",
"confirmations.delete_list.title": "Ka efacar listo?",
"confirmations.discard_edit_media.confirm": "Efacez",
"confirmations.discard_edit_media.message": "Vu havas nesparita chanji di mediodeskript o prevido, vu volas jus efacar?",
"confirmations.discard_edit_media.confirm": "Forigar",
"confirmations.discard_edit_media.message": "Vu havas nekonservita chanji di audvidajpriskribo o prevido, ka forigas ili irgakaze?",
"confirmations.edit.confirm": "Modifikez",
"confirmations.edit.message": "Modifikar nun remplasos la mesajo quon vu nune skribas. Ka vu certe volas procedar?",
"confirmations.edit.title": "Ka remplasar posto?",
"confirmations.follow_to_list.confirm": "Sequar e adjuntar ad listo",
"confirmations.follow_to_list.message": "Vu bezonas sequar {name} por adjuntar lu ad listo.",
"confirmations.follow_to_list.title": "Ka sequar uzanto?",
"confirmations.logout.confirm": "Ekirez",
"confirmations.logout.message": "Ka tu certe volas ekirar?",
"confirmations.logout.title": "Ka ekirar?",
"confirmations.missing_alt_text.confirm": "Adjuntar alternativa texto",
"confirmations.missing_alt_text.message": "Vua afisho enhavas audvidaji sen alternativa texto.",
"confirmations.missing_alt_text.secondary": "Mesajar irgamaniere",
"confirmations.missing_alt_text.title": "Ka adjuntar alternativa texto?",
"confirmations.mute.confirm": "Silencigez",
"confirmations.redraft.confirm": "Efacez e riskisez",
"confirmations.redraft.message": "Ka vu certe volas efacar ca posto e riskisigar ol? Favoriziti e repeti esos perdita, e respondi al posto originala esos orfanigita.",
@ -195,6 +234,7 @@
"confirmations.unfollow.title": "Ka dessequar uzanto?",
"content_warning.hide": "Celez posto",
"content_warning.show": "Montrez nur",
"content_warning.show_more": "Montrar plu",
"conversation.delete": "Efacez konverso",
"conversation.mark_as_read": "Markizez quale lektita",
"conversation.open": "Videz konverso",
@ -210,6 +250,10 @@
"disabled_account_banner.text": "Vua konto {disabledAccount} es nune desaktivigita.",
"dismissable_banner.community_timeline": "Co esas maxim recenta publika posti de personi quo havas konto quo hostigesas da {domain}.",
"dismissable_banner.dismiss": "Ignorez",
"dismissable_banner.explore_links": "Ca novaji es maxim partigita sur fediverso hodie.",
"dismissable_banner.explore_statuses": "Ca afishi di tota fediverso populareskis hodie.",
"dismissable_banner.explore_tags": "Ca gretvorti populareskas sur fediverso hodie.",
"dismissable_banner.public_timeline": "Co es la maxim lastatempa publika afishi di personi sur la fediverso qua personi di {domain} sequas.",
"domain_block_modal.block": "Blokusez servilo",
"domain_block_modal.block_account_instead": "Blokusez @{name} vice",
"domain_block_modal.they_can_interact_with_old_posts": "Personi de ca servilo povas interagar kun vua desnova posti.",
@ -239,7 +283,7 @@
"emoji_button.custom": "Kustumizato",
"emoji_button.flags": "Flagi",
"emoji_button.food": "Manjajo & Drinkajo",
"emoji_button.label": "Insertar emoji",
"emoji_button.label": "Enpozar emocimajo",
"emoji_button.nature": "Naturo",
"emoji_button.not_found": "Nula tala parigata emojii",
"emoji_button.objects": "Kozi",
@ -257,14 +301,14 @@
"empty_column.bookmarked_statuses": "You don't have any bookmarked toots yet. When you bookmark one, it will show up here.",
"empty_column.community": "La lokala tempolineo esas vakua. Skribez ulo publike por iniciar la agiveso!",
"empty_column.direct": "Vu ankore ne havas irga direta mesaji. Kande vu sendos o recevos un, ol montresos hike.",
"empty_column.domain_blocks": "There are no hidden domains yet.",
"empty_column.explore_statuses": "Nulo esas tendenca nun. Videz itere pose!",
"empty_column.domain_blocks": "Ne havas blokusita domeni ankore.",
"empty_column.explore_statuses": "Nulo populareskas nun.",
"empty_column.favourited_statuses": "Vu ankore ne havas irga posti favorizita. Kande vu favorizos un, ol montresos hike.",
"empty_column.favourites": "Nulu favorizis ca posto. Kande ulu favorizis ol, lu montresos hike.",
"empty_column.follow_requests": "Vu ne havas irga sequodemandi til nun. Kande vu ganas talo, ol montresos hike.",
"empty_column.followed_tags": "Vu ankore ne sequis irga hashtago. Kande vu sequos un, ol montresos hike.",
"empty_column.hashtag": "Esas ankore nulo en ta gretovorto.",
"empty_column.home": "Vua hemtempolineo esas vakua! Sequez plu multa personi por plenigar lu. {suggestions}",
"empty_column.home": "Vua hemtempolineo esas desplena!",
"empty_column.list": "There is nothing in this list yet.",
"empty_column.mutes": "Vu ne silencigis irga uzanti til nun.",
"empty_column.notification_requests": "Finis. Kande vu recevas nova savigi, oli aparos hike segun vua preferaji.",
@ -273,7 +317,7 @@
"error.unexpected_crash.explanation": "Pro eroro en nia kodexo o vidilkonciliebloproblemo, ca pagino ne povas korekte montresar.",
"error.unexpected_crash.explanation_addons": "Ca pagino ne povas korekte montresar. Ca eroro posible kauzigesas vidilplusajo o automata tradukutensili.",
"error.unexpected_crash.next_steps": "Probez rifreshar pagino. Se to ne helpas, vu forsan ankore povas uzar Mastodon per diferenta vidilo o provizita softwaro.",
"error.unexpected_crash.next_steps_addons": "Probez desaktivigar e rifreshar pagino. Se to ne helpas, vu forsan ankore povas uzar Mastodon per diferenta vidilo o provizita softwaro.",
"error.unexpected_crash.next_steps_addons": "Probez desebligar ili e rifreshar la pagino.",
"errors.unexpected_crash.copy_stacktrace": "Kopiez amastraso a klipplanko",
"errors.unexpected_crash.report_issue": "Reportigez problemo",
"explore.suggested_follows": "Personi",
@ -281,13 +325,13 @@
"explore.trending_links": "Novaji",
"explore.trending_statuses": "Posti",
"explore.trending_tags": "Hashtagi",
"filter_modal.added.context_mismatch_explanation": "Ca filtrilgrupo ne relatesas kun informo de ca acesesita posto. Se vu volas posto filtresar kun ca informo anke, vu bezonas modifikar filtrilo.",
"filter_modal.added.context_mismatch_explanation": "Ca filtrilgrupo ne uzesis ad informo di ca adirita afisho.",
"filter_modal.added.context_mismatch_title": "Kontenajneparigeso!",
"filter_modal.added.expired_explanation": "Ca filtrilgrupo expiris, vu bezonas chanjar expirtempo por apliko.",
"filter_modal.added.expired_title": "Expirinta filtrilo!",
"filter_modal.added.review_and_configure": "Por kontrolar e plue ajustar ca filtrilgrupo, irez a {settings_link}.",
"filter_modal.added.review_and_configure_title": "Filtrilopcioni",
"filter_modal.added.settings_link": "opcionpagino",
"filter_modal.added.review_and_configure": "Por kontrolar e plu ajustar ca filtrilgrupo, irez a {settings_link}.",
"filter_modal.added.review_and_configure_title": "Filtrilpreferaji",
"filter_modal.added.settings_link": "preferajpagino",
"filter_modal.added.short_explanation": "Ca posto adjuntesas a ca filtrilgrupo: {title}.",
"filter_modal.added.title": "Filtrilo adjuntesas!",
"filter_modal.select_filter.context_mismatch": "ne relatesas kun ca informo",
@ -297,6 +341,7 @@
"filter_modal.select_filter.subtitle": "Usez disponebla grupo o kreez novajo",
"filter_modal.select_filter.title": "Filtragez ca posto",
"filter_modal.title.status": "Filtragez posto",
"filter_warning.matches_filter": "Samas kam filtrilo \"<span>{title}</span>\"",
"filtered_notifications_banner.pending_requests": "De {count, plural,=0 {nulu} one {1 persono} other {# personi}} quan vu forsan konocas",
"filtered_notifications_banner.title": "Filtrilita savigi",
"firehose.all": "Omno",
@ -306,7 +351,7 @@
"follow_request.reject": "Refuzar",
"follow_requests.unlocked_explanation": "Quankam vua konto ne klefklozesis, la {domain} laborero pensas ke vu forsan volas kontralar sequodemandi de ca konti manuale.",
"follow_suggestions.curated_suggestion": "Selektato de jeranto",
"follow_suggestions.dismiss": "Ne montrez pluse",
"follow_suggestions.dismiss": "Ne montrez denove",
"follow_suggestions.featured_longer": "Selektesis da la grupo di {domain}",
"follow_suggestions.friends_of_friends_longer": "Populara inter personi quan vu sequas",
"follow_suggestions.hints.featured": "Ca profilo selektesis da la grupo di {domain}.",
@ -328,13 +373,15 @@
"footer.privacy_policy": "Guidilo pri privateso",
"footer.source_code": "Vidar la fontokodexo",
"footer.status": "Stando",
"footer.terms_of_service": "Serveskondicioni",
"generic.saved": "Sparesis",
"getting_started.heading": "Debuto",
"hashtag.admin_moderation": "Desklozar administrointervizajo por #{name}",
"hashtag.column_header.tag_mode.all": "e {additional}",
"hashtag.column_header.tag_mode.any": "o {additional}",
"hashtag.column_header.tag_mode.none": "sen {additional}",
"hashtag.column_settings.select.no_options_message": "Nula sugestati",
"hashtag.column_settings.select.placeholder": "Insertez hashtagi…",
"hashtag.column_settings.select.placeholder": "Insertez gretvorti…",
"hashtag.column_settings.tag_mode.all": "Omna co",
"hashtag.column_settings.tag_mode.any": "Irga co",
"hashtag.column_settings.tag_mode.none": "Nula co",
@ -342,8 +389,8 @@
"hashtag.counter_by_accounts": "{count, plural, one {{counter} partoprenanto} other {{counter} partoprenanti}}",
"hashtag.counter_by_uses": "{count, plural, one {{counter} posto} other {{counter} posti}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} posto} other {{counter} posti}} hodie",
"hashtag.follow": "Sequez hashtago",
"hashtag.unfollow": "Desequez hashtago",
"hashtag.follow": "Sequar gretvorto",
"hashtag.unfollow": "Dessequar gretvorto",
"hashtags.and_other": "…e {count, plural, one {# plusa}other {# plusa}}",
"hints.profiles.followers_may_be_missing": "Sequanti di ca profilo forsan ne esas hike.",
"hints.profiles.follows_may_be_missing": "Sequati di ca profilo forsan ne esas hike.",
@ -371,12 +418,23 @@
"ignore_notifications_modal.not_followers_title": "Ka ignorar savigi de personi qua ne sequas vu?",
"ignore_notifications_modal.not_following_title": "Ka ignorar savigi de personi quan vu ne sequas?",
"ignore_notifications_modal.private_mentions_title": "Ka ignorar savigi de nekonocita privata mencionii?",
"info_button.label": "Helpo",
"info_button.what_is_alt_text": "<h1>Quo alternativa texto es?</h1> <p>On povas bonigar adireblo e kompreno por omno per skribar klara, deslonga e fakta alternative texto.</p>",
"interaction_modal.action.favourite": "Por durar, vu bezonas prizar de vua konto.",
"interaction_modal.action.follow": "Por durar, vu bezonas sequar de vua konto.",
"interaction_modal.action.reblog": "Por durar, vu bezonas riblogar de vua konto.",
"interaction_modal.action.reply": "Por durar, vu bezonas respondar de vua konto.",
"interaction_modal.action.vote": "Por durar, vu bezonas vocdonar de vua konto.",
"interaction_modal.go": "Irar",
"interaction_modal.no_account_yet": "Ka vu ne havas konto ankore?",
"interaction_modal.on_another_server": "Che diferanta servilo",
"interaction_modal.on_this_server": "Che ca servilo",
"interaction_modal.title.favourite": "Favorizez ca posto da {name}",
"interaction_modal.title.follow": "Sequez {name}",
"interaction_modal.title.reblog": "Repetez posto di {name}",
"interaction_modal.title.reply": "Respondez posto di {name}",
"interaction_modal.title.vote": "Votar sur votinquesto di {name}",
"interaction_modal.username_prompt": "Exemple {example}",
"intervals.full.days": "{number, plural, one {# dio} other {# dii}}",
"intervals.full.hours": "{number, plural, one {# horo} other {# hori}}",
"intervals.full.minutes": "{number, plural, one {# minuto} other {# minuti}}",
@ -401,8 +459,8 @@
"keyboard_shortcuts.muted": "to open muted users list",
"keyboard_shortcuts.my_profile": "to open your profile",
"keyboard_shortcuts.notifications": "to open notifications column",
"keyboard_shortcuts.open_media": "to open media",
"keyboard_shortcuts.pinned": "to open pinned toots list",
"keyboard_shortcuts.open_media": "Desklozar audvidaji",
"keyboard_shortcuts.pinned": "Desklozar listo di adpinglita afishi",
"keyboard_shortcuts.profile": "to open author's profile",
"keyboard_shortcuts.reply": "to reply",
"keyboard_shortcuts.requests": "to open follow requests list",
@ -410,8 +468,9 @@
"keyboard_shortcuts.spoilers": "to show/hide CW field",
"keyboard_shortcuts.start": "to open \"get started\" column",
"keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toggle_sensitivity": "to show/hide media",
"keyboard_shortcuts.toggle_sensitivity": "Montrar/celar audvidaji",
"keyboard_shortcuts.toot": "to start a brand new toot",
"keyboard_shortcuts.translate": "por tradukar mesajo",
"keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
"keyboard_shortcuts.up": "to move up in the list",
"lightbox.close": "Klozar",
@ -424,14 +483,35 @@
"link_preview.author": "Da {name}",
"link_preview.more_from_author": "Plua de {name}",
"link_preview.shares": "{count, plural,one {{counter} posto} other {{counter} posti}}",
"lists.add_member": "Adjuntar",
"lists.add_to_list": "Adjuntar ad listo",
"lists.add_to_lists": "Adjuntar {name} ad listi",
"lists.create": "Krear",
"lists.create_a_list_to_organize": "Krear nova listo por organizar vua hemfluo",
"lists.create_list": "Krear listo",
"lists.delete": "Efacez listo",
"lists.edit": "Modifikez listo",
"lists.done": "Finis",
"lists.edit": "Redaktar listo",
"lists.exclusive": "Celar membri sur Hemo",
"lists.exclusive_hint": "Se ulu es en ca listo, celez lu sur vua hemfluo por evitar vidar lua afishi denove.",
"lists.find_users_to_add": "Serchi uzanti por adjuntar",
"lists.list_members": "Listigar membri",
"lists.list_members_count": "{count, plural,one {# membro} other {#membri}}",
"lists.list_name": "Listonomo",
"lists.new_list_name": "Nova listonomo",
"lists.no_lists_yet": "Nula listi ankore.",
"lists.no_members_yet": "Nula membri ankore.",
"lists.no_results_found": "Nula rezulto trovigesis.",
"lists.remove_member": "Forigar",
"lists.replies_policy.followed": "Irga sequita uzanto",
"lists.replies_policy.list": "Membro di listo",
"lists.replies_policy.none": "Nulu",
"lists.save": "Konservar",
"lists.search": "Serchar",
"lists.show_replies_to": "Inkluzar respondi de listomembri",
"load_pending": "{count, plural, one {# nova kozo} other {# nova kozi}}",
"loading_indicator.label": "Kargante…",
"media_gallery.hide": "Celez",
"media_gallery.hide": "Celar",
"moved_to_account_banner.text": "Vua konto {disabledAccount} es nune desaktiva pro ke vu movis a {movedToAccount}.",
"mute_modal.hide_from_notifications": "Celez de savigi",
"mute_modal.hide_options": "Celez preferaji",
@ -446,12 +526,12 @@
"navigation_bar.administration": "Administro",
"navigation_bar.advanced_interface": "Apertez per retintervizajo",
"navigation_bar.blocks": "Blokusita uzeri",
"navigation_bar.bookmarks": "Libromarki",
"navigation_bar.bookmarks": "Lektosigni",
"navigation_bar.community_timeline": "Lokala tempolineo",
"navigation_bar.compose": "Compose new toot",
"navigation_bar.direct": "Privata mencioni",
"navigation_bar.discover": "Deskovrez",
"navigation_bar.domain_blocks": "Hidden domains",
"navigation_bar.domain_blocks": "Blokusita domeni",
"navigation_bar.explore": "Explorez",
"navigation_bar.favourites": "Favoriziti",
"navigation_bar.filters": "Silencigita vorti",
@ -464,7 +544,7 @@
"navigation_bar.mutes": "Celita uzeri",
"navigation_bar.opened_in_classic_interface": "Posti, konti e altra pagini specifika apertesas en la retovidilo klasika.",
"navigation_bar.personal": "Personala",
"navigation_bar.pins": "Pinned toots",
"navigation_bar.pins": "Adpinglita afishi",
"navigation_bar.preferences": "Preferi",
"navigation_bar.public_timeline": "Federata tempolineo",
"navigation_bar.search": "Serchez",
@ -477,9 +557,14 @@
"notification.admin.report_statuses_other": "{name} raportis {target}",
"notification.admin.sign_up": "{name} registresis",
"notification.admin.sign_up.name_and_others": "{name} e {count, plural,one {# altru} other {#altri}} enrejistris",
"notification.annual_report.message": "Yen vua ye {year} #Wrapstodon!",
"notification.annual_report.view": "Vidar #Wrapstodon",
"notification.favourite": "{name} favorizis tua mesajo",
"notification.favourite.name_and_others_with_link": "{name} e <a>{count, plural,one {# altru} other {# altri}}</a> favorizis vua posto",
"notification.favourite.name_and_others_with_link": "{name} e <a>{count, plural,one {# altru} other {# altri}}</a> stelumis vua afisho",
"notification.favourite_pm": "{name} prizis vua privata menciono",
"notification.favourite_pm.name_and_others_with_link": "{name} e <a>{count, plural,one {# altro} other {# altri}}</a> prizis vua privata menciono",
"notification.follow": "{name} sequeskis tu",
"notification.follow.name_and_others": "{name} e <a>{count, plural,one {# altro} other {# altri}}</a> sequis vu",
"notification.follow_request": "{name} demandas sequar vu",
"notification.follow_request.name_and_others": "{name} e {count, plural,one {# altru} other {# altri}} volas sequar vu",
"notification.label.mention": "Mencionez",
@ -487,37 +572,38 @@
"notification.label.private_reply": "Privata respondo",
"notification.label.reply": "Respondez",
"notification.mention": "Mencionez",
"notification.moderation-warning.learn_more": "Lernez pluse",
"notification.mentioned_you": "{name} mencionis vu",
"notification.moderation-warning.learn_more": "Lernez plu",
"notification.moderation_warning": "Vu recevis jeraverto",
"notification.moderation_warning.action_delete_statuses": "Kelka vua posti efacesis.",
"notification.moderation_warning.action_disable": "Vua konto estas desaktivigita.",
"notification.moderation_warning.action_disable": "Vua konto es desaktivigita.",
"notification.moderation_warning.action_mark_statuses_as_sensitive": "Kelka vua posti markizesis quale sentoza.",
"notification.moderation_warning.action_none": "Vua konto recevis jeraverto.",
"notification.moderation_warning.action_sensitive": "Vua posti markizesos quale sentoza pos nun.",
"notification.moderation_warning.action_silence": "Vua konto limitizesis.",
"notification.moderation_warning.action_suspend": "Vua konto restriktesis.",
"notification.own_poll": "Vua votposto finigis",
"notification.poll": "Votposto quan vu partoprenis finis",
"notification.own_poll": "Vua votinquesto fineskis",
"notification.poll": "Votinquesto ube vu votis fineskis",
"notification.reblog": "{name} repetis tua mesajo",
"notification.reblog.name_and_others_with_link": "{name} e <a>{count, plural,one {# altru} other {#altri}}</a> repetis vua posto",
"notification.relationships_severance_event": "Desganis konekteso kun {name}",
"notification.relationships_severance_event.account_suspension": "Administranto de {from} restriktis {target}, do vu ne povas plue recevar novaji de lu o interagar kun lu.",
"notification.relationships_severance_event.domain_block": "Administranto de {from} blokusis {target}, e anke {followersCount} de vua sequanti e {followingCount, plural, one {# konto} other {# konti}} quan vu sequas.",
"notification.relationships_severance_event.learn_more": "Lernez pluse",
"notification.relationships_severance_event.learn_more": "Lernez plu",
"notification.relationships_severance_event.user_domain_block": "Vu blokusis {target}, do efacis {followersCount} de vua sequanti e {followingCount, plural, one {# konto} other {#konti}} quan vu sequis.",
"notification.status": "{name} nove postigis",
"notification.update": "{name} modifikis posto",
"notification.update": "{name} redaktis afisho",
"notification_requests.accept": "Aceptez",
"notification_requests.accept_multiple": "{count, plural, one {Aceptar # demando…} other {Aceptar # demandi…}}",
"notification_requests.confirm_accept_multiple.button": "{count, plural, one {Aceptar demando} other {Aceptar demandi}}",
"notification_requests.confirm_accept_multiple.message": "Vu aceptos {count, plural, one {1 savigdemando} other {# savigdemandi}}. Ka vu certe volas durar?",
"notification_requests.confirm_accept_multiple.title": "Ka aceptar savigdemandi?",
"notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Ignorez demando} other {Ignorez demandi}}",
"notification_requests.confirm_dismiss_multiple.message": "Vu ignoros {count, plural, one {1 savigdemando} other {# savigdemandi}}. Vu ne povas facile ganar {count, plural, one {ol} other {oli}} pluse. Ka vu esas certe ke vu volas durar?",
"notification_requests.confirm_dismiss_multiple.message": "Vu ignoros {count, plural, one {1 savigdemando} other {# savigdemandi}}. Vu ne povas facile ganar {count, plural, one {ol} other {oli}} denove. Ka vu esas certe ke vu volas durar?",
"notification_requests.confirm_dismiss_multiple.title": "Ka ignorar savigdemandi?",
"notification_requests.dismiss": "Ignorez",
"notification_requests.dismiss_multiple": "{count, plural,one {Ignorez # demando…} other {Ignorez # demandi…}}",
"notification_requests.edit_selection": "Modifikez",
"notification_requests.edit_selection": "Redaktar",
"notification_requests.exit_selection": "Finas",
"notification_requests.explainer_for_limited_account": "Savigi de ca konto filtresis pro ke la konto limitizesis da jeranto.",
"notification_requests.explainer_for_limited_remote_account": "Savigi de ca konto filtresis pro ke la konto o olua servilo limitizesis da jeranto.",
@ -537,8 +623,9 @@
"notifications.column_settings.filter_bar.category": "Rapidfiltrilbaro",
"notifications.column_settings.follow": "Nova sequanti:",
"notifications.column_settings.follow_request": "Nova sequodemandi:",
"notifications.column_settings.group": "Grupo",
"notifications.column_settings.mention": "Mencioni:",
"notifications.column_settings.poll": "Votpostorezulti:",
"notifications.column_settings.poll": "Votinquestorezulti:",
"notifications.column_settings.push": "Pulsavizi",
"notifications.column_settings.reblog": "Repeti:",
"notifications.column_settings.show": "Montrar en kolumno",
@ -546,19 +633,19 @@
"notifications.column_settings.status": "New toots:",
"notifications.column_settings.unread_notifications.category": "Nelektita avizi",
"notifications.column_settings.unread_notifications.highlight": "Briligez nelektita avizi",
"notifications.column_settings.update": "Modifikati:",
"notifications.column_settings.update": "Redaktati:",
"notifications.filter.all": "Omna",
"notifications.filter.boosts": "Repeti",
"notifications.filter.favourites": "Favoriziti",
"notifications.filter.follows": "Sequati",
"notifications.filter.mentions": "Mencioni",
"notifications.filter.polls": "Votpostorezulti",
"notifications.filter.polls": "Votinquestorezulti",
"notifications.filter.statuses": "Novaji de personi quon vu sequas",
"notifications.grant_permission": "Donez permiso.",
"notifications.group": "{count} avizi",
"notifications.mark_as_read": "Markizez singla avizi quale lektita",
"notifications.permission_denied": "Desktopavizi esas nedisplonebla pro antea refuzita vidilpermisdemando",
"notifications.permission_denied_alert": "Desktopavizi ne povas aktivigesar pro ke vidilpermiso refuzesis",
"notifications.permission_denied_alert": "Komputilsavigi ne povas ebligesar, pro ke retumilpermiso desaceptesis antee",
"notifications.permission_required": "Desktopavizi esas nedisplonebla pro ke bezonata permiso ne donesis.",
"notifications.policy.accept": "Aceptez",
"notifications.policy.accept_hint": "Montrez en savigi",
@ -577,10 +664,14 @@
"notifications.policy.filter_private_mentions_hint": "Filtrita se ol ne esas respondo a vua sua menciono o se vu sequas la sendanto",
"notifications.policy.filter_private_mentions_title": "Nekonocita privata mencioni",
"notifications.policy.title": "Regular savigi de…",
"notifications_permission_banner.enable": "Aktivigez desktopavizi",
"notifications_permission_banner.how_to_control": "Por ganar avizi kande Mastodon ne esas apertita, aktivigez dekstopavizi. Vu povas precize regularar quale interakti facas deskstopavizi tra la supera {icon} butono pos oli aktivigesis.",
"notifications_permission_banner.enable": "Ebligar komputilsavigi",
"notifications_permission_banner.how_to_control": "Por ganar savigi kande Mastodon ne es desklozita, ebligez komputilsavigi.",
"notifications_permission_banner.title": "Irga kozo ne pasas vu",
"onboarding.follows.back": "Retro",
"onboarding.follows.done": "Finis",
"onboarding.follows.empty": "Regretinde, nula rezultajo povas montresar nune. Vu povas esforcar serchar, o irar al explorala pagino por trovar personi sequinda, o esforcar itere pose.",
"onboarding.follows.search": "Serchar",
"onboarding.follows.title": "Sequez personi por komencar",
"onboarding.profile.discoverable": "Trovebligez mea profilo",
"onboarding.profile.discoverable_hint": "Se vu selektas deskovrebleso che Mastodon, vua posti povas aparar en sercho-rezultaji e populari, e vua profilo forsan sugestesos a personi kun interesi simila a vua.",
"onboarding.profile.display_name": "Publika nomo",
@ -595,28 +686,31 @@
"password_confirmation.mismatching": "La konfirmo dil pasvorto ne egalesas",
"picture_in_picture.restore": "Retropozez",
"poll.closed": "Klozita",
"poll.refresh": "Rifreshez",
"poll.refresh": "Rifreshar",
"poll.reveal": "Vidar rezultaji",
"poll.total_people": "{count, plural, one {# persono} other {# personi}}",
"poll.total_votes": "{count, plural, one {# voto} other {# voti}}",
"poll.vote": "Votez",
"poll.vote": "Votar",
"poll.voted": "Vu ja votis ca respondo",
"poll.votes": "{votes, plural, one {# voto} other {# voti}}",
"poll_button.add_poll": "Insertez votposto",
"poll_button.remove_poll": "Efacez votposto",
"poll_button.add_poll": "Adjuntar votinquesto",
"poll_button.remove_poll": "Forigar votinquesto",
"privacy.change": "Aranjar privateso di mesaji",
"privacy.direct.long": "Omnu quan mencionesis en la posto",
"privacy.direct.short": "Privata menciono",
"privacy.private.long": "Nur vua sequanti",
"privacy.private.short": "Sequanti",
"privacy.public.long": "Ulu de e ne de Mastodon",
"privacy.public.short": "Publike",
"privacy.unlisted.additional": "Co kondutas exakte kam publika, escepte la posto ne aparos en viva novajari o gretiketi, exploro, o sercho di Mastodon, mem se vu esas volunta totkonte.",
"privacy.unlisted.additional": "Co kondutas exakte kam publika, ecepte la posto ne aparos en nuna flui o gretvorti, exploro, o sercho di Mastodon, mem se vu esas volunta totkonte.",
"privacy.unlisted.long": "Min multa algoritmoridikuli",
"privacy.unlisted.short": "Deslauta publiko",
"privacy_policy.last_updated": "Antea novajo ye {date}",
"privacy_policy.title": "Privatesguidilo",
"recommended": "Rekomendata",
"refresh": "Rifreshez",
"regeneration_indicator.please_stand_by": "Vartez.",
"regeneration_indicator.preparing_your_home_feed": "Preparas vua hemfluo…",
"relative_time.days": "{number}d",
"relative_time.full.days": "{number, plural, one {# dio} other {# dii}} ante nun",
"relative_time.full.hours": "{number, plural, one {# horo} other {# hori}} ante nun",
@ -628,9 +722,9 @@
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"relative_time.today": "hodie",
"reply_indicator.attachments": "{count, plural, one {# atachajo} other {# atachaji}}",
"reply_indicator.attachments": "{count, plural, one {# addonajo} other {# addonaji}}",
"reply_indicator.cancel": "Nihiligar",
"reply_indicator.poll": "Votposto",
"reply_indicator.poll": "Votinquesto",
"report.block": "Restriktez",
"report.block_explanation": "Vu ne vidos olia posti. Oli ne povas vidar vua posti o sequar vu. Oli savos ke oli restriktesis.",
"report.categories.legal": "Legala",
@ -665,13 +759,13 @@
"report.statuses.title": "Ka existas irga posti quo suportas ca raporto?",
"report.submit": "Sendar",
"report.target": "Denuncante",
"report.thanks.take_action": "Co esas vua opcioni por regularar quo vu vidas che Mastodon:",
"report.thanks.take_action": "Yen vua preferaji por regularar quon vu vidas sur Mastodon:",
"report.thanks.take_action_actionable": "Dum ke ni kontrolas co, vu povas demarshar kontra @{name}:",
"report.thanks.title": "Ka vu ne volas vidar co?",
"report.thanks.title_actionable": "Danko por raportizar, ni kontrolos co.",
"report.unfollow": "Desequez @{name}",
"report.unfollow_explanation": "Vu sequas ca konto. Por ne vidar olia posti en vua hemniuzeto pluse, desequez oli.",
"report_notification.attached_statuses": "{count, plural,one {{count} posti} other {{count} posti}} adjuntesas",
"report.unfollow_explanation": "Vu sequas ca konto.",
"report_notification.attached_statuses": "{count, plural,one {{count} posti} other {{count} posti}} addonita",
"report_notification.categories.legal": "Legala",
"report_notification.categories.legal_sentence": "deslegala kontenajo",
"report_notification.categories.other": "Altra",
@ -699,9 +793,12 @@
"search_popout.user": "uzanto",
"search_results.accounts": "Profili",
"search_results.all": "Omna",
"search_results.hashtags": "Hashtagi",
"search_results.hashtags": "Gretvorti",
"search_results.no_results": "Nula rezulto.",
"search_results.no_search_yet": "Probez serchar afishi, profili o gretvorti.",
"search_results.see_all": "Videz omni",
"search_results.statuses": "Posti",
"search_results.title": "Serchar \"{q}\"",
"server_banner.about_active_users": "Personi quo uzas ca servilo dum antea 30 dii (monate aktiva uzanti)",
"server_banner.active_users": "aktiva uzanti",
"server_banner.administered_by": "Administresis da:",
@ -716,7 +813,7 @@
"status.admin_domain": "Apertez jerintervizajo por {domain}",
"status.admin_status": "Open this status in the moderation interface",
"status.block": "Restriktez @{name}",
"status.bookmark": "Libromarko",
"status.bookmark": "Lektosigno",
"status.cancel_reblog_private": "Desrepetez",
"status.cannot_reblog": "Ca posto ne povas repetesar",
"status.continued_thread": "Durigita postaro",
@ -725,34 +822,35 @@
"status.detailed_status": "Detala konversvido",
"status.direct": "Private mencionez @{name}",
"status.direct_indicator": "Privata menciono",
"status.edit": "Modifikez",
"status.edited": "Recente modifikesis ye {date}",
"status.edited_x_times": "Modifikesis {count, plural, one {{count} foyo} other {{count} foyi}}",
"status.edit": "Redaktar",
"status.edited": "Lastatempe redaktesar ye {date}",
"status.edited_x_times": "Redaktesis ye {count, plural, one {{count} foyo} other {{count} foyi}}",
"status.embed": "Ganez adherkodexo",
"status.favourite": "Favorizar",
"status.favourites": "{count, plural, one {favorizo} other {favorizi}}",
"status.favourites": "{count, plural, one {stelumo} other {stelumi}}",
"status.filter": "Filtragez ca posto",
"status.history.created": "{name} kreis ye {date}",
"status.history.edited": "{name} modifikis ye {date}",
"status.load_more": "Kargar pluse",
"status.history.edited": "{name} redaktis ye {date}",
"status.load_more": "Kargar plu",
"status.media.open": "Klikez por apertar",
"status.media.show": "Klikez por montrar",
"status.media_hidden": "Kontenajo celita",
"status.media_hidden": "Audvidaji es celita",
"status.mention": "Mencionar @{name}",
"status.more": "Pluse",
"status.more": "Plu",
"status.mute": "Silencigez @{name}",
"status.mute_conversation": "Silencigez konverso",
"status.open": "Detaligar ca mesajo",
"status.pin": "Pinglagez che profilo",
"status.pinned": "Pinned toot",
"status.read_more": "Lektez pluse",
"status.pinned": "Adpinglita afisho",
"status.read_more": "Lektez plu",
"status.reblog": "Repetez",
"status.reblog_private": "Repetez kun originala videbleso",
"status.reblogged_by": "{name} repetis",
"status.reblogs": "{count, plural, one {repeto} other {repeti}}",
"status.reblogs.empty": "Nulu ja repetis ca posto. Kande ulu facas lo, lu montresos hike.",
"status.redraft": "Efacez e riskisigez",
"status.remove_bookmark": "Efacez libromarko",
"status.remove_bookmark": "Forigar lektosigno",
"status.remove_favourite": "Forigar de priziti",
"status.replied_in_thread": "Respondesis en postaro",
"status.replied_to": "Respondis a {name}",
"status.reply": "Respondar",
@ -761,9 +859,9 @@
"status.sensitive_warning": "Trubliva kontenajo",
"status.share": "Partigez",
"status.show_less_all": "Montrez min por omno",
"status.show_more_all": "Montrez pluse por omno",
"status.show_more_all": "Montrez plu por omno",
"status.show_original": "Montrez originalo",
"status.title.with_attachments": "{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}",
"status.title.with_attachments": "{user} afishis {attachmentCount, plural, one {addonajo} other {{attachmentCount} addonaji}}",
"status.translate": "Tradukez",
"status.translated_from_with": "Tradukita de {lang} per {provider}",
"status.uncached_media_warning": "Previdajo nedisponebla",
@ -774,32 +872,33 @@
"subscribed_languages.target": "Chanjez abonita lingui por {target}",
"tabs_bar.home": "Hemo",
"tabs_bar.notifications": "Savigi",
"terms_of_service.title": "Servtermini",
"time_remaining.days": "{number, plural, one {# dio} other {# dii}} restas",
"time_remaining.hours": "{number, plural, one {# horo} other {# hori}} restas",
"time_remaining.minutes": "{number, plural, one {# minuto} other {# minuti}} restas",
"time_remaining.moments": "Poka sekundi restas",
"time_remaining.seconds": "{number, plural, one {# sekundo} other {# sekundi}} restas",
"trends.counter_by_accounts": "{count, plural,one {{counter} persono} other {{counter} personi}} en antea {days, plural,one {dio} other {{days} dii}}",
"trends.trending_now": "Tendencigas nun",
"trends.trending_now": "Populareskas nun",
"ui.beforeunload": "Vua skisato perdesos se vu ekiras Mastodon.",
"units.short.billion": "{count}G",
"units.short.million": "{count}M",
"units.short.thousand": "{count}K",
"upload_area.title": "Tranar faligar por kargar",
"upload_button.label": "Adjuntar kontenajo",
"upload_error.limit": "Failadcharglimito ecesesis.",
"upload_error.poll": "Failadchargo ne permisesas kun votposti.",
"upload_form.drag_and_drop.instructions": "Por tenar mediatachajo, presez spaco o eniro. Presez spaco o eniro itere por destenar la mediatachajo en olua nova loko, o presez eskapo por anular.",
"upload_form.drag_and_drop.on_drag_cancel": "Tiro anulesis. Mediatachajo {item} destenesis.",
"upload_form.drag_and_drop.on_drag_end": "Mediatachajo {item} destenesis.",
"upload_form.drag_and_drop.on_drag_over": "Mediatachajo {item} movigesis.",
"upload_form.drag_and_drop.on_drag_start": "Tenis mediatachajo {item}.",
"upload_form.edit": "Modifikez",
"upload_progress.label": "Kargante...",
"upload_area.title": "Tenar e destenar por adkargar",
"upload_button.label": "Adjuntar imaji, video o sondosiero",
"upload_error.limit": "Dosieradkarglimito ecesesis.",
"upload_error.poll": "Dosieradkargo ne permisesas kun votinquesti.",
"upload_form.drag_and_drop.instructions": "Por tenar audvidajaddonajo, presez spaco o eniro. Presez spaco o eniro denove por destenar la audvidajatachajo en olua nova loko, o presez eskapo por nuligar.",
"upload_form.drag_and_drop.on_drag_cancel": "Tiro nuligesis.",
"upload_form.drag_and_drop.on_drag_end": "Audvidajaddonajo {item} destenesis.",
"upload_form.drag_and_drop.on_drag_over": "Audvidajaddonajo {item} movigesis.",
"upload_form.drag_and_drop.on_drag_start": "Tenis audvidajaddonajo {item}.",
"upload_form.edit": "Redaktar",
"upload_progress.label": "Adkargas...",
"upload_progress.processing": "Traktante…",
"username.taken": "Ta uzantnomo ja es posedita. Provez altro",
"video.close": "Klozez video",
"video.download": "Deschargez failo",
"video.download": "Deschargar dosiero",
"video.exit_fullscreen": "Ekirez plena skreno",
"video.expand": "Expansez video",
"video.fullscreen": "Plena skreno",

View file

@ -598,11 +598,11 @@
"notification_requests.confirm_accept_multiple.button": "{count, plural, one {Samþykkja beiðni} other {Samþykkja beiðnir}}",
"notification_requests.confirm_accept_multiple.message": "Þú ert að fara að samþykkja {count, plural, one {eina beiðni um tilkynningar} other {# beiðnir um tilkynningar}}. Ertu viss um að þú viljir halda áfram?",
"notification_requests.confirm_accept_multiple.title": "Samþykkja beiðnir um tilkynningar?",
"notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Afgreiða beiðni} other {Afgreiða beiðnir}}",
"notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Hafna beiðni} other {Hafna beiðnum}}",
"notification_requests.confirm_dismiss_multiple.message": "Þú ert að fara að hunsa {count, plural, one {eina beiðni um tilkynningar} other {# beiðnir um tilkynningar}}. Þú munt ekki eiga auðvelt með að skoða {count, plural, one {hana} other {þær}} aftur síðar. Ertu viss um að þú viljir halda áfram?",
"notification_requests.confirm_dismiss_multiple.title": "Hunsa beiðnir um tilkynningar?",
"notification_requests.dismiss": "Afgreiða",
"notification_requests.dismiss_multiple": "{count, plural, one {Afgreiða # beiðni…} other {Afgreiða # beiðnir…}}",
"notification_requests.dismiss": "Hafna",
"notification_requests.dismiss_multiple": "{count, plural, one {Hafna # beiðni…} other {Hafna # beiðnum…}}",
"notification_requests.edit_selection": "Breyta",
"notification_requests.exit_selection": "Lokið",
"notification_requests.explainer_for_limited_account": "Tilkynningar frá þessum notanda hafa verið síaðar þar sem aðgangur hans hefur verið takmarkaður af umsjónarmanni.",
@ -872,7 +872,9 @@
"subscribed_languages.target": "Breyta tungumálum í áskrift fyrir {target}",
"tabs_bar.home": "Heim",
"tabs_bar.notifications": "Tilkynningar",
"terms_of_service.effective_as_of": "Gildir frá og með {date}",
"terms_of_service.title": "Þjónustuskilmálar",
"terms_of_service.upcoming_changes_on": "Væntanlegar breytingar þann {date}",
"time_remaining.days": "{number, plural, one {# dagur} other {# dagar}} eftir",
"time_remaining.hours": "{number, plural, one {# klukkustund} other {# klukkustundir}} eftir",
"time_remaining.minutes": "{number, plural, one {# mínúta} other {# mínútur}} eftir",

View file

@ -872,7 +872,9 @@
"subscribed_languages.target": "Modifica le lingue in cui sei iscritto per {target}",
"tabs_bar.home": "Home",
"tabs_bar.notifications": "Notifiche",
"terms_of_service.effective_as_of": "In vigore a partire dal giorno {date}",
"terms_of_service.title": "Termini di Servizio",
"terms_of_service.upcoming_changes_on": "Prossime modifiche nel giorno {date}",
"time_remaining.days": "{number, plural, one {# giorno} other {# giorni}} left",
"time_remaining.hours": "{number, plural, one {# ora} other {# ore}} left",
"time_remaining.minutes": "{number, plural, one {# minuto} other {# minuti}} left",

View file

@ -219,7 +219,7 @@
"confirmations.logout.message": "本当にログアウトしますか?",
"confirmations.logout.title": "ログアウトしようとしています",
"confirmations.missing_alt_text.confirm": "代替テキストを追加",
"confirmations.missing_alt_text.message": "あなたの投稿には大体テキストのないメディアが含まれています。説明文を追加することで、より多くの人がコンテンツにアクセスできるようになります。",
"confirmations.missing_alt_text.message": "あなたの投稿には代替テキストのないメディアが含まれています。説明文を追加することで、より多くの人がコンテンツにアクセスできるようになります。",
"confirmations.missing_alt_text.secondary": "そのまま投稿する",
"confirmations.missing_alt_text.title": "代替テキストを追加しますか?",
"confirmations.mute.confirm": "ミュート",

View file

@ -872,6 +872,7 @@
"subscribed_languages.target": "{target}에 대한 구독 언어 변경",
"tabs_bar.home": "홈",
"tabs_bar.notifications": "알림",
"terms_of_service.effective_as_of": "{date}부터 적용됨",
"terms_of_service.title": "이용 약관",
"time_remaining.days": "{number} 일 남음",
"time_remaining.hours": "{number} 시간 남음",

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