diff --git a/.nvmrc b/.nvmrc index f88da62e246007..b0195acf7815c7 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -24.11 +24.12 diff --git a/.ruby-version b/.ruby-version index 2aa51319921198..7921bd0c892723 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.4.7 +3.4.8 diff --git a/.storybook/main.ts b/.storybook/main.ts index c249d1c06dcff1..2f70c80dbfe00a 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -27,6 +27,7 @@ const config: StorybookConfig = { 'oops.gif', 'oops.png', ].map((path) => ({ from: `../public/${path}`, to: `/${path}` })), + { from: '../app/javascript/images/logo.svg', to: '/custom-emoji/logo.svg' }, ], viteFinal(config) { // For an unknown reason, Storybook does not use the root diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index d66f0fb11a84d3..abbd193c68190e 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -11,6 +11,11 @@ import type { Preview } from '@storybook/react-vite'; import { initialize, mswLoader } from 'msw-storybook-addon'; import { action } from 'storybook/actions'; +import { + importCustomEmojiData, + importLegacyShortcodes, + importEmojiData, +} from '@/mastodon/features/emoji/loader'; import type { LocaleData } from '@/mastodon/locales'; import { reducerWithInitialState } from '@/mastodon/reducers'; import { defaultMiddleware } from '@/mastodon/store/store'; @@ -50,12 +55,31 @@ const preview: Preview = { locale: 'en', }, decorators: [ - (Story, { parameters, globals, args }) => { + (Story, { parameters, globals, args, argTypes }) => { // Get the locale from the global toolbar // and merge it with any parameters or args state. const { locale } = globals as { locale: string }; const { state = {} } = parameters; - const { state: argsState = {} } = args; + + const argsState: Record = {}; + for (const [key, value] of Object.entries(args)) { + const argType = argTypes[key]; + if (argType?.reduxPath) { + const reduxPath = Array.isArray(argType.reduxPath) + ? argType.reduxPath.map((p) => p.toString()) + : argType.reduxPath.split('.'); + + reduxPath.reduce((acc, key, i) => { + if (acc[key] === undefined) { + acc[key] = {}; + } + if (i === reduxPath.length - 1) { + acc[key] = value; + } + return acc[key] as Record; + }, argsState); + } + } const reducer = reducerWithInitialState( { @@ -64,7 +88,7 @@ const preview: Preview = { }, }, state as Record, - argsState as Record, + argsState, ); const store = configureStore({ @@ -127,7 +151,12 @@ const preview: Preview = { ), ], - loaders: [mswLoader], + loaders: [ + mswLoader, + importCustomEmojiData, + importLegacyShortcodes, + ({ globals: { locale } }) => importEmojiData(locale as string), + ], parameters: { layout: 'centered', diff --git a/.storybook/storybook-addon-vitest.d.ts b/.storybook/storybook.d.ts similarity index 54% rename from .storybook/storybook-addon-vitest.d.ts rename to .storybook/storybook.d.ts index 86852faca9f8f4..47624d1e9c6783 100644 --- a/.storybook/storybook-addon-vitest.d.ts +++ b/.storybook/storybook.d.ts @@ -1,7 +1,20 @@ // The addon package.json incorrectly exports types, so we need to override them here. + +import type { RootState } from '@/mastodon/store'; + // See: https://github.com/storybookjs/storybook/blob/v9.0.4/code/addons/vitest/package.json#L70-L76 declare module '@storybook/addon-vitest/vitest-plugin' { export * from '@storybook/addon-vitest/dist/vitest-plugin/index'; } +type RootPathKeys = keyof RootState; + +declare module 'storybook/internal/csf' { + export interface InputType { + reduxPath?: + | `${RootPathKeys}.${string}` + | [RootPathKeys, ...(string | number)[]]; + } +} + export {}; diff --git a/.yarn/patches/babel-plugin-lodash-npm-3.3.4-c7161075b6.patch b/.yarn/patches/babel-plugin-lodash-npm-3.3.4-c7161075b6.patch deleted file mode 100644 index 0b3f94d09ee83a..00000000000000 --- a/.yarn/patches/babel-plugin-lodash-npm-3.3.4-c7161075b6.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/lib/index.js b/lib/index.js -index 16ed6be8be8f555cc99096c2ff60954b42dc313d..d009c069770d066ad0db7ad02de1ea473a29334e 100644 ---- a/lib/index.js -+++ b/lib/index.js -@@ -99,7 +99,7 @@ function lodash(_ref) { - - var node = _ref3; - -- if ((0, _types.isModuleDeclaration)(node)) { -+ if ((0, _types.isImportDeclaration)(node) || (0, _types.isExportDeclaration)(node)) { - isModule = true; - break; - } diff --git a/CHANGELOG.md b/CHANGELOG.md index 45bb26b5140b8b..399b2fe0844b58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,26 @@ All notable changes to this project will be documented in this file. +## [4.5.3] - 2025-12-08 + +### Security + +- Fix inconsistent error handling leaking information on existence of private posts ([GHSA-gwhw-gcjx-72v8](https://github.com/mastodon/mastodon/security/advisories/GHSA-gwhw-gcjx-72v8)) + +### Fixed + +- Fix “Delete and Redraft” on a non-quote being treated as a quote post in some cases (#37140 by @ClearlyClaire) +- Fix YouTube embeds by sending referer (#37126 by @ChaosExAnima) +- Fix streamed quoted polls not being hydrated correctly (#37118 by @ClearlyClaire) +- Fix creation of duplicate conversations (#37108 by @oneiros) +- Fix extraneous `noreferrer` in external links (#37107 by @ChaosExAnima) +- Fix edge case error handling in some database migrations (#37079 by @ClearlyClaire) +- Fix error handling when re-fetching already-known statuses (#37077 by @ClearlyClaire) +- Fix post navigation in single-column mode when Advanced UI is enabled (#37044 by @diondiondion) +- Fix `tootctl status remove` removing quoted posts and remote quotes of local posts (#37009 by @ClearlyClaire) +- Fix known expensive S3 batch delete operation failing because of short timeouts (#37004 by @ClearlyClaire) +- Fix compose autosuggest always lowercasing input token (#36995 by @ClearlyClaire) + ## [4.5.2] - 2025-11-20 ### Changed diff --git a/Dockerfile b/Dockerfile index c64d529918d0b0..865d14402cd88e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ 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.7" +ARG RUBY_VERSION="3.4.8" # # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="22"] # renovate: datasource=node-version depName=node ARG NODE_MAJOR_VERSION="24" diff --git a/Gemfile b/Gemfile index 60dc80a9bc0494..0a7c00b11e343b 100644 --- a/Gemfile +++ b/Gemfile @@ -138,7 +138,7 @@ group :test do # Browser integration testing gem 'capybara', '~> 3.39' gem 'capybara-playwright-driver' - gem 'playwright-ruby-client', '1.56.0', require: false # Pinning the exact version as it needs to be kept in sync with the installed npm package + gem 'playwright-ruby-client', '1.57.0', require: false # Pinning the exact version as it needs to be kept in sync with the installed npm package # Used to reset the database between system tests gem 'database_cleaner-active_record' diff --git a/Gemfile.lock b/Gemfile.lock index ada28236f67854..20e4ff9fe71b6f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -53,7 +53,7 @@ GEM erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - active_model_serializers (0.10.15) + active_model_serializers (0.10.16) actionpack (>= 4.1) activemodel (>= 4.1) case_transform (>= 0.2) @@ -96,7 +96,7 @@ GEM ast (2.4.3) attr_required (1.0.2) aws-eventstream (1.4.0) - aws-partitions (1.1190.0) + aws-partitions (1.1194.0) aws-sdk-core (3.239.2) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) @@ -108,7 +108,7 @@ GEM aws-sdk-kms (1.118.0) aws-sdk-core (~> 3, >= 3.239.1) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.206.0) + aws-sdk-s3 (1.207.0) aws-sdk-core (~> 3, >= 3.234.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) @@ -166,7 +166,7 @@ GEM climate_control (1.2.0) cocoon (1.2.15) color_diff (0.1) - concurrent-ruby (1.3.5) + concurrent-ruby (1.3.6) connection_pool (2.5.5) cose (1.3.1) cbor (~> 0.5.9) @@ -182,7 +182,7 @@ GEM activerecord (>= 5.a) database_cleaner-core (~> 2.0) database_cleaner-core (2.0.1) - date (3.5.0) + date (3.5.1) debug (1.11.0) irb (~> 1.10) reline (>= 0.3.8) @@ -208,7 +208,7 @@ GEM domain_name (0.6.20240107) doorkeeper (5.8.2) railties (>= 5) - dotenv (3.1.8) + dotenv (3.2.0) drb (2.2.3) dry-cli (1.3.0) elasticsearch (7.17.11) @@ -227,11 +227,11 @@ GEM mail (~> 2.7) email_validator (2.2.4) activemodel - erb (5.1.3) + erb (6.0.1) erubi (1.13.1) et-orbi (1.4.0) tzinfo - excon (1.3.0) + excon (1.3.2) logger fabrication (3.0.0) faker (3.5.3) @@ -244,8 +244,8 @@ GEM faraday (>= 1, < 3) faraday-httpclient (2.0.2) httpclient (>= 2.2) - faraday-net_http (3.4.1) - net-http (>= 0.5.0) + faraday-net_http (3.4.2) + net-http (~> 0.5) fast_blank (1.0.1) fastimage (2.4.0) ffi (1.17.2) @@ -269,20 +269,20 @@ GEM fog-openstack (1.1.5) fog-core (~> 2.1) fog-json (>= 1.0) - formatador (1.2.1) + formatador (1.2.3) reline forwardable (1.3.3) - fugit (1.12.0) + fugit (1.12.1) et-orbi (~> 1.4) raabro (~> 1.4) globalid (1.3.0) activesupport (>= 6.1) - google-protobuf (4.32.1) + google-protobuf (4.33.2) bigdecimal rake (>= 13) googleapis-common-protos-types (1.22.0) google-protobuf (~> 4.26) - haml (6.4.0) + haml (7.1.0) temple (>= 0.8.2) thor tilt @@ -291,7 +291,7 @@ GEM activesupport (>= 5.1) haml (>= 4.0.6) railties (>= 5.1) - haml_lint (0.67.0) + haml_lint (0.68.0) haml (>= 5.0) parallel (~> 1.10) rainbow @@ -304,8 +304,8 @@ GEM highline (3.1.2) reline hiredis (0.6.3) - hiredis-client (0.26.1) - redis-client (= 0.26.1) + hiredis-client (0.26.2) + redis-client (= 0.26.2) hkdf (0.3.0) htmlentities (4.3.4) http (5.3.1) @@ -340,7 +340,7 @@ GEM inline_svg (1.10.0) activesupport (>= 3.0) nokogiri (>= 1.6) - io-console (0.8.1) + io-console (0.8.2) irb (1.15.3) pp (>= 0.6.0) rdoc (>= 4.0.0) @@ -350,7 +350,7 @@ GEM azure-blob (~> 0.5.2) hashie (~> 5.0) jmespath (1.6.2) - json (2.16.0) + json (2.18.0) json-canonicalization (1.0.0) json-jwt (1.17.0) activesupport (>= 4.2) @@ -447,13 +447,13 @@ GEM mime-types-data (3.2025.0924) mini_mime (1.1.5) mini_portile2 (2.8.9) - minitest (5.26.2) + minitest (5.27.0) msgpack (1.8.0) - multi_json (1.17.0) + multi_json (1.18.0) mutex_m (0.3.0) net-http (0.6.0) uri - net-imap (0.5.12) + net-imap (0.6.0) date net-protocol net-ldap (0.20.0) @@ -465,11 +465,11 @@ GEM timeout net-smtp (0.5.1) net-protocol - nio4r (2.7.4) + nio4r (2.7.5) nokogiri (1.18.10) mini_portile2 (~> 2.8.2) racc (~> 1.4) - oj (3.16.12) + oj (3.16.13) bigdecimal (>= 3.0) ostruct (>= 0.2) omniauth (2.1.4) @@ -481,7 +481,7 @@ GEM addressable (~> 2.8) nokogiri (~> 1.12) omniauth (~> 2.1) - omniauth-rails_csrf_protection (2.0.0) + omniauth-rails_csrf_protection (2.0.1) actionpack (>= 4.2) omniauth (~> 2.0) omniauth-saml (2.2.4) @@ -594,7 +594,7 @@ GEM pg (1.6.2) pghero (3.7.0) activerecord (>= 7.1) - playwright-ruby-client (1.56.0) + playwright-ruby-client (1.57.0) concurrent-ruby (>= 1.1.6) mime-types (>= 3.0) pp (0.6.3) @@ -615,7 +615,7 @@ GEM actionpack (>= 7.0.0) activesupport (>= 7.0.0) rack - psych (5.2.6) + psych (5.3.0) date stringio public_suffix (7.0.0) @@ -631,7 +631,7 @@ GEM rack-cors (3.0.0) logger rack (>= 3.0.14) - rack-oauth2 (2.2.1) + rack-oauth2 (2.3.0) activesupport attr_required faraday (~> 2.0) @@ -649,7 +649,7 @@ GEM rack (>= 3.0.0) rack-test (2.2.0) rack (>= 1.3) - rackup (2.2.1) + rackup (2.3.1) rack (>= 3) rails (8.0.3) actioncable (= 8.0.3) @@ -695,7 +695,7 @@ GEM readline (~> 0.0) rdf-normalize (0.7.0) rdf (~> 3.3) - rdoc (6.15.1) + rdoc (6.17.0) erb psych (>= 4.0.0) tsort @@ -703,7 +703,7 @@ GEM reline redcarpet (3.6.1) redis (4.8.1) - redis-client (0.26.1) + redis-client (0.26.2) connection_pool regexp_parser (2.11.3) reline (0.6.3) @@ -721,18 +721,18 @@ GEM chunky_png (~> 1.0) rqrcode_core (~> 2.0) rqrcode_core (2.0.1) - rspec (3.13.1) + rspec (3.13.2) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) rspec-mocks (~> 3.13.0) - rspec-core (3.13.5) + rspec-core (3.13.6) rspec-support (~> 3.13.0) rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-github (3.0.0) rspec-core (~> 3.0) - rspec-mocks (3.13.5) + rspec-mocks (3.13.7) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-rails (8.0.2) @@ -773,7 +773,7 @@ GEM lint_roller (~> 1.1) rubocop (>= 1.75.0, < 2.0) rubocop-ast (>= 1.47.1, < 2.0) - rubocop-rails (2.33.4) + rubocop-rails (2.34.2) activesupport (>= 4.2.0) lint_roller (~> 1.1) rack (>= 1.1) @@ -792,7 +792,7 @@ GEM ruby-saml (1.18.1) nokogiri (>= 1.13.10) rexml - ruby-vips (2.2.5) + ruby-vips (2.3.0) ffi (~> 1.12) logger rubyzip (3.2.2) @@ -820,7 +820,7 @@ GEM sidekiq-scheduler (6.0.1) rufus-scheduler (~> 3.2) sidekiq (>= 7.3, < 9) - sidekiq-unique-jobs (8.0.11) + sidekiq-unique-jobs (8.0.12) concurrent-ruby (~> 1.0, >= 1.0.5) sidekiq (>= 7.0.0, < 9.0.0) thor (>= 1.0, < 3.0) @@ -839,9 +839,10 @@ GEM stackprof (0.2.27) starry (0.2.0) base64 - stoplight (5.6.0) + stoplight (5.7.0) + concurrent-ruby zeitwerk - stringio (3.1.8) + stringio (3.1.9) strong_migrations (2.5.1) activerecord (>= 7.1) swd (2.0.3) @@ -855,10 +856,10 @@ GEM unicode-display_width (>= 1.1.1, < 4) terrapin (1.1.1) climate_control - test-prof (1.4.4) + test-prof (1.5.0) thor (1.4.0) tilt (2.6.1) - timeout (0.4.3) + timeout (0.5.0) tpm-key_attestation (0.14.1) bindata (~> 2.4) openssl (> 2.0) @@ -879,7 +880,7 @@ GEM unf (~> 0.1.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - tzinfo-data (1.2025.2) + tzinfo-data (1.2025.3) tzinfo (>= 1.0.0) unf (0.1.4) unf_ext @@ -919,7 +920,7 @@ GEM addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - webrick (1.9.1) + webrick (1.9.2) websocket-driver (0.8.0) base64 websocket-extensions (>= 0.1.0) @@ -1032,7 +1033,7 @@ DEPENDENCIES parslet pg (~> 1.5) pghero - playwright-ruby-client (= 1.56.0) + playwright-ruby-client (= 1.57.0) premailer-rails prometheus_exporter (~> 2.2) propshaft @@ -1089,7 +1090,7 @@ DEPENDENCIES xorcist (~> 1.1) RUBY VERSION - ruby 3.4.1p0 + ruby 3.4.1p0 BUNDLED WITH - 2.7.2 + 4.0.2 diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb index 49cfc8ad1cbd8c..1f7abb97fa5750 100644 --- a/app/controllers/activitypub/inboxes_controller.rb +++ b/app/controllers/activitypub/inboxes_controller.rb @@ -39,7 +39,7 @@ def body return @body if defined?(@body) @body = request.body.read - @body.force_encoding('UTF-8') if @body.present? + @body.presence&.force_encoding('UTF-8') request.body.rewind if request.body.respond_to?(:rewind) diff --git a/app/controllers/api/v1/annual_reports_controller.rb b/app/controllers/api/v1/annual_reports_controller.rb index 724c7658d7247b..71a97e1d9a68b2 100644 --- a/app/controllers/api/v1/annual_reports_controller.rb +++ b/app/controllers/api/v1/annual_reports_controller.rb @@ -57,27 +57,18 @@ def read render_empty end - def refresh_key - "wrapstodon:#{current_account.id}:#{year}" - end - private def report_state - return 'available' if GeneratedAnnualReport.exists?(account_id: current_account.id, year: year) - - async_refresh = AsyncRefresh.new(refresh_key) - - if async_refresh.running? + AnnualReport.new(current_account, year).state do |async_refresh| add_async_refresh_header(async_refresh, retry_seconds: 2) - 'generating' - elsif AnnualReport.current_campaign == year && AnnualReport.new(current_account, year).eligible? - 'eligible' - else - 'ineligible' end end + def refresh_key + "wrapstodon:#{current_account.id}:#{year}" + end + def year params[:id]&.to_i end diff --git a/app/controllers/api/v1/lists_controller.rb b/app/controllers/api/v1/lists_controller.rb index b019ab601882e1..79282c862fd0d0 100644 --- a/app/controllers/api/v1/lists_controller.rb +++ b/app/controllers/api/v1/lists_controller.rb @@ -28,7 +28,7 @@ def update def destroy antenna = Antenna.find_by(list_id: @list.id) - antenna.update!(list_id: 0) if antenna.present? + antenna.presence&.update!(list_id: 0) @list.destroy! render_empty diff --git a/app/controllers/api/v1/statuses/quotes_controller.rb b/app/controllers/api/v1/statuses/quotes_controller.rb index be3a4edc83d87a..d851e55c293fef 100644 --- a/app/controllers/api/v1/statuses/quotes_controller.rb +++ b/app/controllers/api/v1/statuses/quotes_controller.rb @@ -41,8 +41,8 @@ def set_statuses if current_account&.id != @status.account_id domains = @statuses.filter_map(&:account_domain).uniq account_ids = @statuses.map(&:account_id).uniq - relations = current_account&.relations_map(account_ids, domains) || {} - @statuses.reject! { |status| StatusFilter.new(status, current_account, relations).filtered? } + current_account&.preload_relations!(account_ids, domains) + @statuses.reject! { |status| StatusFilter.new(status, current_account).filtered? } end end diff --git a/app/controllers/api/v1/statuses/referred_by_statuses_controller.rb b/app/controllers/api/v1/statuses/referred_by_statuses_controller.rb index c13c5ff0e8519d..fafc64293deb4a 100644 --- a/app/controllers/api/v1/statuses/referred_by_statuses_controller.rb +++ b/app/controllers/api/v1/statuses/referred_by_statuses_controller.rb @@ -29,9 +29,8 @@ def results account = current_user&.account statuses = Status.where(id: @status.referenced_by_status_objects.select(:status_id)) - account_ids = statuses.map(&:account_id).uniq - domains = statuses.filter_map(&:account_domain).uniq - relations = account&.relations_map(account_ids, domains) || {} + statuses.map(&:account_id).uniq + statuses.filter_map(&:account_domain).uniq statuses = preload_collection_paginated_by_id( statuses, @@ -40,7 +39,7 @@ def results params_slice(:max_id, :since_id, :min_id) ) - @results = statuses.filter { |status| !StatusFilter.new(status, account, relations).filtered? } + @results = statuses.filter { |status| !StatusFilter.new(status, account).filtered? } end def insert_pagination_headers diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 0aeb84c1220f9a..98368139e0478d 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -27,6 +27,7 @@ class Api::V1::StatusesController < Api::BaseController ANCESTORS_LIMIT = 40 DESCENDANTS_LIMIT = 60 DESCENDANTS_DEPTH_LIMIT = 20 + REFERENCES_LIMIT = 20 def index @statuses = preload_collection(@statuses, Status) @@ -45,16 +46,18 @@ def context ancestors_limit = CONTEXT_LIMIT descendants_limit = CONTEXT_LIMIT descendants_depth_limit = nil + references_limit = CONTEXT_LIMIT if current_account.nil? ancestors_limit = ANCESTORS_LIMIT descendants_limit = DESCENDANTS_LIMIT descendants_depth_limit = DESCENDANTS_DEPTH_LIMIT + references_limit = REFERENCES_LIMIT end ancestors_results = @status.in_reply_to_id.nil? ? [] : @status.ancestors(ancestors_limit, current_account) descendants_results = @status.descendants(descendants_limit, current_account, descendants_depth_limit) - references_results = @status.readable_references(current_account) + references_results = @status.readable_references(references_limit, current_account) loaded_ancestors = preload_collection(ancestors_results, Status) loaded_descendants = preload_collection(descendants_results, Status) loaded_references = preload_collection(references_results, Status) diff --git a/app/controllers/api/v1_alpha/collection_items_controller.rb b/app/controllers/api/v1_alpha/collection_items_controller.rb new file mode 100644 index 00000000000000..21699a5b6f2676 --- /dev/null +++ b/app/controllers/api/v1_alpha/collection_items_controller.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +class Api::V1Alpha::CollectionItemsController < Api::BaseController + include Authorization + + before_action :check_feature_enabled + + before_action -> { doorkeeper_authorize! :write, :'write:collections' } + + before_action :require_user! + + before_action :set_collection + before_action :set_account, only: [:create] + before_action :set_collection_item, only: [:destroy] + + after_action :verify_authorized + + def create + authorize @collection, :update? + authorize @account, :feature? + + @item = AddAccountToCollectionService.new.call(@collection, @account) + + render json: @item, serializer: REST::CollectionItemSerializer + end + + def destroy + authorize @collection, :update? + + @collection_item.destroy + + head 200 + end + + private + + def set_collection + @collection = Collection.find(params[:collection_id]) + end + + def set_account + return render(json: { error: '`account_id` parameter is missing' }, status: 422) if params[:account_id].blank? + + @account = Account.find(params[:account_id]) + end + + def set_collection_item + @collection_item = @collection.collection_items.find(params[:id]) + end + + def check_feature_enabled + raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled? + end +end diff --git a/app/controllers/api/v1_alpha/collections_controller.rb b/app/controllers/api/v1_alpha/collections_controller.rb index 65c08d8d6bca00..d0c4e0f3f04c6d 100644 --- a/app/controllers/api/v1_alpha/collections_controller.rb +++ b/app/controllers/api/v1_alpha/collections_controller.rb @@ -71,7 +71,6 @@ def set_account def set_collections @collections = @account.collections .with_tag - .with_item_count .order(created_at: :desc) .offset(offset_param) .limit(limit_param(DEFAULT_COLLECTIONS_LIMIT)) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 82d9e8380fc854..efee54cb7bb50a 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -18,6 +18,8 @@ class ApplicationController < ActionController::Base helper_method :current_account helper_method :current_session helper_method :current_theme + helper_method :color_scheme + helper_method :contrast helper_method :single_user_mode? helper_method :use_seamless_external_login? helper_method :sso_account_settings @@ -177,6 +179,25 @@ def current_theme current_user.setting_theme end + def color_scheme + current = current_user&.setting_color_scheme + return current if current && current != 'auto' + + return 'dark' if current_theme.include?('default') || current_theme.include?('contrast') + return 'light' if current_theme.include?('light') + + 'auto' + end + + def contrast + current = current_user&.setting_contrast + return current if current && current != 'auto' + + return 'high' if current_theme.include?('contrast') + + 'auto' + end + def respond_with_error(code) respond_to do |format| format.any { render "errors/#{code}", layout: 'error', status: code, formats: [:html] } diff --git a/app/controllers/concerns/api/interaction_policies_concern.rb b/app/controllers/concerns/api/interaction_policies_concern.rb index f1e1480c0c0cb9..0679c3c691e41f 100644 --- a/app/controllers/concerns/api/interaction_policies_concern.rb +++ b/app/controllers/concerns/api/interaction_policies_concern.rb @@ -6,9 +6,9 @@ module Api::InteractionPoliciesConcern def quote_approval_policy case status_params[:quote_approval_policy].presence || current_user.setting_default_quote_policy when 'public' - Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16 + InteractionPolicy::POLICY_FLAGS[:public] << 16 when 'followers' - Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16 + InteractionPolicy::POLICY_FLAGS[:followers] << 16 when 'nobody' 0 else diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index 2bdd3558643526..1e83ab9c69b6b7 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -72,10 +72,13 @@ def signed_request_actor rescue Mastodon::SignatureVerificationError => e fail_with! e.message rescue *Mastodon::HTTP_CONNECTION_ERRORS => e + @signature_verification_failure_code ||= 503 fail_with! "Failed to fetch remote data: #{e.message}" rescue Mastodon::UnexpectedResponseError + @signature_verification_failure_code ||= 503 fail_with! 'Failed to fetch remote data (got unexpected reply from server)' rescue Stoplight::Error::RedLight + @signature_verification_failure_code ||= 503 fail_with! 'Fetching attempt skipped because of recent connection failure' end diff --git a/app/controllers/wrapstodon_controller.rb b/app/controllers/wrapstodon_controller.rb index 74f0dbb65ae775..b1fe521fb114d5 100644 --- a/app/controllers/wrapstodon_controller.rb +++ b/app/controllers/wrapstodon_controller.rb @@ -12,7 +12,7 @@ class WrapstodonController < ApplicationController skip_before_action :require_functional!, only: :show, unless: :limited_federation_mode? def show - expires_in 10.seconds, public: true if current_account.nil? + expires_in 10.minutes, public: true if current_account.nil? end private diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index da201625190039..36d0779fffe92d 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -157,6 +157,7 @@ def opengraph(property, content) def html_classes output = [] + output << content_for(:html_classes) output << 'system-font' if current_account&.user&.setting_system_font_ui output << 'custom-scrollbars' unless current_account&.user&.setting_system_scrollbars_ui output << (current_account&.user&.setting_reduce_motion ? 'reduce-motion' : 'no-reduce-motion') diff --git a/app/helpers/database_helper.rb b/app/helpers/database_helper.rb index 62a26a0c2a05c8..f245d303d10ba9 100644 --- a/app/helpers/database_helper.rb +++ b/app/helpers/database_helper.rb @@ -2,7 +2,7 @@ module DatabaseHelper def replica_enabled? - ENV['REPLICA_DB_NAME'] || ENV.fetch('REPLICA_DATABASE_URL', nil) + ENV['REPLICA_DB_NAME'] || ENV['REPLICA_DB_HOST'] || ENV.fetch('REPLICA_DATABASE_URL', nil) end module_function :replica_enabled? diff --git a/app/helpers/filters_helper.rb b/app/helpers/filters_helper.rb index 22a1c172de2c86..ec0d55788327d7 100644 --- a/app/helpers/filters_helper.rb +++ b/app/helpers/filters_helper.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true module FiltersHelper + KEYWORDS_LIMIT = 5 + def filter_action_label(action) safe_join( [ @@ -9,4 +11,10 @@ def filter_action_label(action) ] ) end + + def filter_keywords(filter) + filter.keywords.map(&:keyword).take(KEYWORDS_LIMIT).tap do |list| + list << '…' if filter.keywords.size > KEYWORDS_LIMIT + end.join(', ') + end end diff --git a/app/helpers/wrapstodon_helper.rb b/app/helpers/wrapstodon_helper.rb index da3b0d6fad86a8..5a0075a0e58be8 100644 --- a/app/helpers/wrapstodon_helper.rb +++ b/app/helpers/wrapstodon_helper.rb @@ -2,15 +2,20 @@ module WrapstodonHelper def render_wrapstodon_share_data(report) - json = ActiveModelSerializers::SerializableResource.new( + payload = ActiveModelSerializers::SerializableResource.new( AnnualReportsPresenter.new([report]), serializer: REST::AnnualReportsSerializer, scope: nil, scope_name: :current_user - ).to_json + ).as_json + + payload[:me] = current_account.id.to_s if user_signed_in? + payload[:domain] = Addressable::IDNA.to_unicode(Rails.configuration.x.local_domain) + + json_string = payload.to_json # rubocop:disable Rails/OutputSafety - content_tag(:script, json_escape(json).html_safe, type: 'application/json', id: 'wrapstodon-data') + content_tag(:script, json_escape(json_string).html_safe, type: 'application/json', id: 'wrapstodon-data') # rubocop:enable Rails/OutputSafety end end diff --git a/app/javascript/entrypoints/wrapstodon.tsx b/app/javascript/entrypoints/wrapstodon.tsx index e1eebcce576985..9fff41a1330438 100644 --- a/app/javascript/entrypoints/wrapstodon.tsx +++ b/app/javascript/entrypoints/wrapstodon.tsx @@ -2,13 +2,11 @@ import { createRoot } from 'react-dom/client'; import { Provider as ReduxProvider } from 'react-redux'; -import { - importFetchedAccounts, - importFetchedStatuses, -} from '@/mastodon/actions/importer'; +import { importFetchedStatuses } from '@/mastodon/actions/importer'; +import { hydrateStore } from '@/mastodon/actions/store'; import type { ApiAnnualReportResponse } from '@/mastodon/api/annual_report'; import { Router } from '@/mastodon/components/router'; -import { WrapstodonShare } from '@/mastodon/features/annual_report/share'; +import { WrapstodonSharedPage } from '@/mastodon/features/annual_report/shared_page'; import { IntlProvider, loadLocale } from '@/mastodon/locales'; import { loadPolyfills } from '@/mastodon/polyfills'; import ready from '@/mastodon/ready'; @@ -27,13 +25,24 @@ function loaded() { const initialState = JSON.parse( propsNode.textContent, - ) as ApiAnnualReportResponse; + ) as ApiAnnualReportResponse & { me?: string; domain: string }; const report = initialState.annual_reports[0]; if (!report) { throw new Error('Initial state report not found'); } - store.dispatch(importFetchedAccounts(initialState.accounts)); + + // Set up store + store.dispatch( + hydrateStore({ + meta: { + locale: document.documentElement.lang, + me: initialState.me, + domain: initialState.domain, + }, + accounts: initialState.accounts, + }), + ); store.dispatch(importFetchedStatuses(initialState.statuses)); store.dispatch(setReport(report)); @@ -43,7 +52,7 @@ function loaded() { - + , diff --git a/app/javascript/fonts/silkscreen-wrapstodon/OFL.txt b/app/javascript/fonts/silkscreen-wrapstodon/OFL.txt new file mode 100644 index 00000000000000..63c1c98e1e92b7 --- /dev/null +++ b/app/javascript/fonts/silkscreen-wrapstodon/OFL.txt @@ -0,0 +1,100 @@ +Below you'll find the original License file for the Silkscreen font. +The file used on Mastodon is a custom file subset to only include the +characters "Wrapstodon 0123456789" using the Font Squirrel Font-face Generator +(https://www.fontsquirrel.com/tools/webfont-generator) + +----------------------------------------------------------- + +Copyright 2001 The Silkscreen Project Authors (https://github.com/googlefonts/silkscreen) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/app/javascript/fonts/silkscreen-wrapstodon/silkscreen-regular.woff2 b/app/javascript/fonts/silkscreen-wrapstodon/silkscreen-regular.woff2 new file mode 100644 index 00000000000000..3b7ba43e9c36f5 Binary files /dev/null and b/app/javascript/fonts/silkscreen-wrapstodon/silkscreen-regular.woff2 differ diff --git a/app/javascript/images/archetypes/previews/booster.jpg b/app/javascript/images/archetypes/previews/booster.jpg new file mode 100644 index 00000000000000..fe74640b2e0e19 Binary files /dev/null and b/app/javascript/images/archetypes/previews/booster.jpg differ diff --git a/app/javascript/images/archetypes/previews/lurker.jpg b/app/javascript/images/archetypes/previews/lurker.jpg new file mode 100644 index 00000000000000..ba1897a2a95c6f Binary files /dev/null and b/app/javascript/images/archetypes/previews/lurker.jpg differ diff --git a/app/javascript/images/archetypes/previews/oracle.jpg b/app/javascript/images/archetypes/previews/oracle.jpg new file mode 100644 index 00000000000000..a18de034f45d52 Binary files /dev/null and b/app/javascript/images/archetypes/previews/oracle.jpg differ diff --git a/app/javascript/images/archetypes/previews/pollster.jpg b/app/javascript/images/archetypes/previews/pollster.jpg new file mode 100644 index 00000000000000..c53e5418671542 Binary files /dev/null and b/app/javascript/images/archetypes/previews/pollster.jpg differ diff --git a/app/javascript/images/archetypes/previews/replier.jpg b/app/javascript/images/archetypes/previews/replier.jpg new file mode 100644 index 00000000000000..086ee599798fc4 Binary files /dev/null and b/app/javascript/images/archetypes/previews/replier.jpg differ diff --git a/app/javascript/images/archetypes/space_elements.png b/app/javascript/images/archetypes/space_elements.png new file mode 100644 index 00000000000000..8b83506b8e7336 Binary files /dev/null and b/app/javascript/images/archetypes/space_elements.png differ diff --git a/app/javascript/images/icons/icon_planet.svg b/app/javascript/images/icons/icon_planet.svg new file mode 100644 index 00000000000000..e8bf34c7a66717 --- /dev/null +++ b/app/javascript/images/icons/icon_planet.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 4a648afe4b843b..2fc26a43b05d9c 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -696,7 +696,16 @@ export function selectComposeSuggestion(position, token, suggestion, path) { dispatch(useEmoji(suggestion)); } else if (suggestion.type === 'hashtag') { - completion = token + suggestion.name.slice(token.length - 1); + // TODO: it could make sense to keep the “most capitalized” of the two + const tokenName = token.slice(1); // strip leading '#' + const suggestionPrefix = suggestion.name.slice(0, tokenName.length); + const prefixMatchesSuggestion = suggestionPrefix.localeCompare(tokenName, undefined, { sensitivity: 'accent' }) === 0; + if (prefixMatchesSuggestion) { + completion = token + suggestion.name.slice(tokenName.length); + } else { + completion = `${token.slice(0, 1)}${suggestion.name}`; + } + startPosition = position - 1; } else if (suggestion.type === 'account') { completion = `@${getState().getIn(['accounts', suggestion.id, 'acct'])}`; diff --git a/app/javascript/mastodon/actions/importer/emoji.ts b/app/javascript/mastodon/actions/importer/emoji.ts new file mode 100644 index 00000000000000..85043718cf43fb --- /dev/null +++ b/app/javascript/mastodon/actions/importer/emoji.ts @@ -0,0 +1,23 @@ +import type { ApiCustomEmojiJSON } from '@/mastodon/api_types/custom_emoji'; +import { loadCustomEmoji } from '@/mastodon/features/emoji'; + +export async function importCustomEmoji(emojis: ApiCustomEmojiJSON[]) { + if (emojis.length === 0) { + return; + } + + // First, check if we already have them all. + const { searchCustomEmojisByShortcodes, clearEtag } = await import( + '@/mastodon/features/emoji/database' + ); + + const existingEmojis = await searchCustomEmojisByShortcodes( + emojis.map((emoji) => emoji.shortcode), + ); + + // If there's a mismatch, re-import all custom emojis. + if (existingEmojis.length < emojis.length) { + await clearEtag('custom'); + await loadCustomEmoji(); + } +} diff --git a/app/javascript/mastodon/actions/importer/index.js b/app/javascript/mastodon/actions/importer/index.js index 09820827e6f0ce..e400f4a03f2cea 100644 --- a/app/javascript/mastodon/actions/importer/index.js +++ b/app/javascript/mastodon/actions/importer/index.js @@ -1,6 +1,7 @@ import { createPollFromServerJSON } from 'mastodon/models/poll'; import { importAccounts } from './accounts'; +import { importCustomEmoji } from './emoji'; import { normalizeStatus } from './normalizer'; import { importPolls } from './polls'; @@ -39,6 +40,10 @@ export function importFetchedAccounts(accounts) { if (account.moved) { processAccount(account.moved); } + + if (account.emojis && account.username === account.acct) { + importCustomEmoji(account.emojis); + } } accounts.forEach(processAccount); @@ -82,6 +87,10 @@ export function importFetchedStatuses(statuses, outsideOptions = {}) { if (status.card) { status.card.authors.forEach(author => author.account && pushUnique(accounts, author.account)); } + + if (status.emojis && status.account.username === status.account.acct) { + importCustomEmoji(status.emojis); + } } statuses.forEach(processStatus); diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index 853e23977586d5..526a4ae11f2c63 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -2,6 +2,8 @@ import escapeTextContentForBrowser from 'escape-html'; import { expandSpoilers, me } from '../../initial_state'; +import { importCustomEmoji } from './emoji'; + const domParser = new DOMParser(); export function searchTextFromRawStatus (status) { @@ -178,5 +180,9 @@ export function normalizeAnnouncement(announcement) { normalAnnouncement.contentHtml = normalAnnouncement.content; + if (normalAnnouncement.emojis) { + importCustomEmoji(normalAnnouncement.emojis); + } + return normalAnnouncement; } diff --git a/app/javascript/mastodon/actions/search.ts b/app/javascript/mastodon/actions/search.ts index 1e57c307157d41..4f21a53b4d8230 100644 --- a/app/javascript/mastodon/actions/search.ts +++ b/app/javascript/mastodon/actions/search.ts @@ -144,7 +144,7 @@ export const hydrateSearch = createAppAsyncThunk( 'search/hydrate', (_args, { dispatch, getState }) => { const me = getState().meta.get('me') as string; - const history = searchHistory.get(me) as RecentSearch[] | null; + const history = searchHistory.get(me); if (history !== null) { dispatch(updateSearchHistory(history)); diff --git a/app/javascript/mastodon/actions/server.js b/app/javascript/mastodon/actions/server.js index 47b6e7a1764dc9..32ee093afa8423 100644 --- a/app/javascript/mastodon/actions/server.js +++ b/app/javascript/mastodon/actions/server.js @@ -1,5 +1,3 @@ -import { checkAnnualReport } from '@/mastodon/reducers/slices/annual_report'; - import api from '../api'; import { importFetchedAccount } from './importer'; @@ -31,9 +29,6 @@ export const fetchServer = () => (dispatch, getState) => { .get('/api/v2/instance').then(({ data }) => { if (data.contact.account) dispatch(importFetchedAccount(data.contact.account)); dispatch(fetchServerSuccess(data)); - if (data.wrapstodon) { - void dispatch(checkAnnualReport()); - } }).catch(err => dispatch(fetchServerFail(err))); }; diff --git a/app/javascript/mastodon/actions/store.js b/app/javascript/mastodon/actions/store.js index e8fec134535a82..7a68679d4477a5 100644 --- a/app/javascript/mastodon/actions/store.js +++ b/app/javascript/mastodon/actions/store.js @@ -22,6 +22,8 @@ export function hydrateStore(rawState) { dispatch(hydrateCompose()); dispatch(hydrateSearch()); - dispatch(importFetchedAccounts(Object.values(rawState.accounts))); + if (rawState.accounts) { + dispatch(importFetchedAccounts(Object.values(rawState.accounts))); + } }; } diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index ff62049c3fcfd4..f48f257d620787 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -6,7 +6,7 @@ import { usePendingItems as preferPendingItems } from 'mastodon/initial_state'; import { importFetchedStatus, importFetchedStatuses } from './importer'; import { submitMarkers } from './markers'; -import {timelineDelete} from './timelines_typed'; +import { timelineDelete } from './timelines_typed'; export { disconnectTimeline } from './timelines_typed'; @@ -24,9 +24,15 @@ export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; export const TIMELINE_MARK_AS_PARTIAL = 'TIMELINE_MARK_AS_PARTIAL'; export const TIMELINE_INSERT = 'TIMELINE_INSERT'; +// When adding new special markers here, make sure to update TIMELINE_NON_STATUS_MARKERS in actions/timelines_typed.js export const TIMELINE_SUGGESTIONS = 'inline-follow-suggestions'; export const TIMELINE_GAP = null; +export const TIMELINE_NON_STATUS_MARKERS = [ + TIMELINE_GAP, + TIMELINE_SUGGESTIONS, +]; + export const loadPending = timeline => ({ type: TIMELINE_LOAD_PENDING, timeline, diff --git a/app/javascript/mastodon/actions/timelines_typed.ts b/app/javascript/mastodon/actions/timelines_typed.ts index 07d82b2f015a7c..e8468826603dec 100644 --- a/app/javascript/mastodon/actions/timelines_typed.ts +++ b/app/javascript/mastodon/actions/timelines_typed.ts @@ -2,6 +2,12 @@ import { createAction } from '@reduxjs/toolkit'; import { usePendingItems as preferPendingItems } from 'mastodon/initial_state'; +import { TIMELINE_NON_STATUS_MARKERS } from './timelines'; + +export function isNonStatusId(value: unknown) { + return TIMELINE_NON_STATUS_MARKERS.includes(value as string | null); +} + export const disconnectTimeline = createAction( 'timeline/disconnect', ({ timeline }: { timeline: string }) => ({ diff --git a/app/javascript/mastodon/components/account/account.stories.tsx b/app/javascript/mastodon/components/account/account.stories.tsx index 3a3a255b7fc582..050ed6e9005967 100644 --- a/app/javascript/mastodon/components/account/account.stories.tsx +++ b/app/javascript/mastodon/components/account/account.stories.tsx @@ -1,16 +1,28 @@ +import type { ComponentProps } from 'react'; + import type { Meta, StoryObj } from '@storybook/react-vite'; import { accountFactoryState, relationshipsFactory } from '@/testing/factories'; import { Account } from './index'; +type Props = Omit, 'id'> & { + name: string; + username: string; +}; + const meta = { title: 'Components/Account', - component: Account, argTypes: { - id: { + name: { + type: 'string', + description: 'The display name of the account', + reduxPath: 'accounts.1.display_name_html', + }, + username: { type: 'string', - description: 'ID of the account to display', + description: 'The username of the account', + reduxPath: 'accounts.1.acct', }, size: { type: 'number', @@ -40,7 +52,8 @@ const meta = { }, }, args: { - id: '1', + name: 'Test User', + username: 'testuser', size: 46, hidden: false, minimal: false, @@ -55,17 +68,16 @@ const meta = { }, }, }, -} satisfies Meta; + render(args) { + return ; + }, +} satisfies Meta; export default meta; type Story = StoryObj; -export const Primary: Story = { - args: { - id: '1', - }, -}; +export const Primary: Story = {}; export const Hidden: Story = { args: { diff --git a/app/javascript/mastodon/components/content_warning.tsx b/app/javascript/mastodon/components/content_warning.tsx index a407ec146e3e12..2d3222f479a50c 100644 --- a/app/javascript/mastodon/components/content_warning.tsx +++ b/app/javascript/mastodon/components/content_warning.tsx @@ -31,7 +31,7 @@ export const ContentWarning: React.FC<{ } + extraEmojis={status.get('emojis') as List} /> ); diff --git a/app/javascript/mastodon/components/dismissable_banner.tsx b/app/javascript/mastodon/components/dismissable_banner.tsx index a874f4792e4c7f..39ae11422a3c2b 100644 --- a/app/javascript/mastodon/components/dismissable_banner.tsx +++ b/app/javascript/mastodon/components/dismissable_banner.tsx @@ -1,12 +1,10 @@ -import type { PropsWithChildren } from 'react'; -import { useCallback, useState, useEffect } from 'react'; +import type { FC, ReactNode } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import CloseIcon from '@/material-icons/400-24px/close.svg?react'; -import { changeSetting } from 'mastodon/actions/settings'; -import { bannerSettings } from 'mastodon/settings'; -import { useAppSelector, useAppDispatch } from 'mastodon/store'; + +import { useDismissible } from '../hooks/useDismissible'; import { IconButton } from './icon_button'; @@ -16,48 +14,12 @@ const messages = defineMessages({ interface Props { id: string; + children: ReactNode; } -export function useDismissableBannerState({ id }: Props) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const dismissed: boolean = useAppSelector((state) => - /* eslint-disable-next-line */ - state.settings.getIn(['dismissed_banners', id], false), - ); - - const [isVisible, setIsVisible] = useState( - !bannerSettings.get(id) && !dismissed, - ); - - const dispatch = useAppDispatch(); - - const dismiss = useCallback(() => { - setIsVisible(false); - bannerSettings.set(id, true); - dispatch(changeSetting(['dismissed_banners', id], true)); - }, [id, dispatch]); - - useEffect(() => { - // Store legacy localStorage setting on server - if (!isVisible && !dismissed) { - dispatch(changeSetting(['dismissed_banners', id], true)); - } - }, [id, dispatch, isVisible, dismissed]); - - return { - wasDismissed: !isVisible, - dismiss, - }; -} - -export const DismissableBanner: React.FC> = ({ - id, - children, -}) => { +export const DismissableBanner: FC = ({ id, children }) => { const intl = useIntl(); - const { wasDismissed, dismiss } = useDismissableBannerState({ - id, - }); + const { wasDismissed, dismiss } = useDismissible(id); if (wasDismissed) { return null; diff --git a/app/javascript/mastodon/components/emoji/emoji.stories.tsx b/app/javascript/mastodon/components/emoji/emoji.stories.tsx index a5e283158d6121..e7d1887aa9a3a8 100644 --- a/app/javascript/mastodon/components/emoji/emoji.stories.tsx +++ b/app/javascript/mastodon/components/emoji/emoji.stories.tsx @@ -2,24 +2,27 @@ import type { ComponentProps } from 'react'; import type { Meta, StoryObj } from '@storybook/react-vite'; -import { importCustomEmojiData } from '@/mastodon/features/emoji/loader'; +import { customEmojiFactory } from '@/testing/factories'; +import { CustomEmojiProvider } from './context'; import { Emoji } from './index'; -type EmojiProps = ComponentProps & { state: string }; +type EmojiProps = ComponentProps & { + style: 'auto' | 'native' | 'twemoji'; +}; const meta = { title: 'Components/Emoji', component: Emoji, args: { code: '🖤', - state: 'auto', + style: 'auto', }, argTypes: { code: { name: 'Emoji', }, - state: { + style: { control: { type: 'select', labels: { @@ -30,16 +33,15 @@ const meta = { }, options: ['auto', 'native', 'twemoji'], name: 'Emoji Style', - mapping: { - auto: { meta: { emoji_style: 'auto' } }, - native: { meta: { emoji_style: 'native' } }, - twemoji: { meta: { emoji_style: 'twemoji' } }, - }, + reduxPath: 'meta.emoji_style', }, }, render(args) { - void importCustomEmojiData(); - return ; + return ( + + + + ); }, } satisfies Meta; diff --git a/app/javascript/mastodon/components/emoji/index.tsx b/app/javascript/mastodon/components/emoji/index.tsx index dd0f258f848a30..1fdf22f0f6ab4c 100644 --- a/app/javascript/mastodon/components/emoji/index.tsx +++ b/app/javascript/mastodon/components/emoji/index.tsx @@ -3,7 +3,10 @@ import { useContext, useEffect, useState } from 'react'; import classNames from 'classnames'; -import { EMOJI_TYPE_CUSTOM } from '@/mastodon/features/emoji/constants'; +import { + EMOJI_TYPE_CUSTOM, + EMOJI_TYPE_UNICODE, +} from '@/mastodon/features/emoji/constants'; import { useEmojiAppState } from '@/mastodon/features/emoji/mode'; import { emojiToInversionClassName, @@ -47,8 +50,6 @@ export const Emoji: FC = ({ const animate = useContext(AnimateEmojiContext); - const inversionClass = emojiToInversionClassName(code); - const fallback = showFallback ? code : null; // If the code is invalid or we otherwise know it's not valid, show the fallback. @@ -56,10 +57,6 @@ export const Emoji: FC = ({ return fallback; } - if (!shouldRenderImage(state, appState.mode)) { - return code; - } - if (!isStateLoaded(state)) { if (showLoading) { return ; @@ -67,6 +64,17 @@ export const Emoji: FC = ({ return fallback; } + const inversionClass = + state.type === EMOJI_TYPE_UNICODE && + emojiToInversionClassName(state.data.unicode); + + if (!shouldRenderImage(state, appState.mode)) { + if (state.type === EMOJI_TYPE_UNICODE) { + return state.data.unicode; + } + return code; + } + if (state.type === EMOJI_TYPE_CUSTOM) { const shortcode = `:${state.code}:`; const style = diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index 23fe18079e4ced..95852470b9d669 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -125,6 +125,7 @@ class Status extends ImmutablePureComponent { hidden: PropTypes.bool, unread: PropTypes.bool, showThread: PropTypes.bool, + showActions: PropTypes.bool, isQuotedPost: PropTypes.bool, shouldHighlightOnMount: PropTypes.bool, getScrollPosition: PropTypes.func, @@ -391,7 +392,7 @@ class Status extends ImmutablePureComponent { }; render () { - const { intl, hidden, featured, unfocusable, unread, showThread, isQuotedPost = false, scrollKey, pictureInPicture, previousId, nextInReplyToId, rootId, skipPrepend, avatarSize = 46, children } = this.props; + const { intl, hidden, featured, unfocusable, unread, showThread, showActions = true, isQuotedPost = false, scrollKey, pictureInPicture, previousId, nextInReplyToId, rootId, skipPrepend, avatarSize = 46, children } = this.props; let { status, account, ...other } = this.props; @@ -660,7 +661,7 @@ class Status extends ImmutablePureComponent { )} - {!isQuotedPost && (!matchedFilters || this.state.showDespiteFilter) && + {(showActions && !isQuotedPost) && (!matchedFilters || this.state.showDespiteFilter) && } diff --git a/app/javascript/mastodon/components/status/intercept_status_clicks.tsx b/app/javascript/mastodon/components/status/intercept_status_clicks.tsx new file mode 100644 index 00000000000000..b0dbc3c693848c --- /dev/null +++ b/app/javascript/mastodon/components/status/intercept_status_clicks.tsx @@ -0,0 +1,45 @@ +import { useCallback, useRef } from 'react'; + +export const InterceptStatusClicks: React.FC<{ + onPreventedClick: ( + clickedArea: 'account' | 'post', + event: React.MouseEvent, + ) => void; + children: React.ReactNode; +}> = ({ onPreventedClick, children }) => { + const wrapperRef = useRef(null); + + const handleClick = useCallback( + (e: React.MouseEvent) => { + const clickTarget = e.target as Element; + const allowedElementsSelector = + '.video-player, .audio-player, .media-gallery, .content-warning'; + const allowedElements = wrapperRef.current?.querySelectorAll( + allowedElementsSelector, + ); + const isTargetClickAllowed = + allowedElements && + Array.from(allowedElements).some((element) => { + return clickTarget === element || element.contains(clickTarget); + }); + + if (!isTargetClickAllowed) { + e.preventDefault(); + e.stopPropagation(); + + const wasAccountAreaClicked = !!clickTarget.closest( + 'a.status__display-name', + ); + + onPreventedClick(wasAccountAreaClicked ? 'account' : 'post', e); + } + }, + [onPreventedClick], + ); + + return ( +
+ {children} +
+ ); +}; diff --git a/app/javascript/mastodon/components/status_action_bar/remove_quote_hint.module.css b/app/javascript/mastodon/components/status_action_bar/remove_quote_hint.module.css new file mode 100644 index 00000000000000..5045b6d1b95590 --- /dev/null +++ b/app/javascript/mastodon/components/status_action_bar/remove_quote_hint.module.css @@ -0,0 +1,3 @@ +.inlineIcon { + vertical-align: middle; +} diff --git a/app/javascript/mastodon/components/status_action_bar/remove_quote_hint.tsx b/app/javascript/mastodon/components/status_action_bar/remove_quote_hint.tsx index 1c5cfeddccc58b..cfa72feb6843a5 100644 --- a/app/javascript/mastodon/components/status_action_bar/remove_quote_hint.tsx +++ b/app/javascript/mastodon/components/status_action_bar/remove_quote_hint.tsx @@ -6,13 +6,15 @@ import classNames from 'classnames'; import Overlay from 'react-overlays/Overlay'; +import { useDismissible } from '@/mastodon/hooks/useDismissible'; import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; import { Button } from '../button'; -import { useDismissableBannerState } from '../dismissable_banner'; import { Icon } from '../icon'; -const DISMISSABLE_BANNER_ID = 'notifications/remove_quote_hint'; +import classes from './remove_quote_hint.module.css'; + +const DISMISSIBLE_BANNER_ID = 'notifications/remove_quote_hint'; /** * We don't want to show this hint in the UI more than once, @@ -29,9 +31,7 @@ export const RemoveQuoteHint: React.FC<{ const anchorRef = useRef(null); const intl = useIntl(); - const { wasDismissed, dismiss } = useDismissableBannerState({ - id: DISMISSABLE_BANNER_ID, - }); + const { wasDismissed, dismiss } = useDismissible(DISMISSIBLE_BANNER_ID); const shouldShowHint = !wasDismissed && canShowHint; @@ -94,7 +94,7 @@ export const RemoveQuoteHint: React.FC<{ id: 'status.more', defaultMessage: 'More', })} - style={{ verticalAlign: 'middle' }} + className={classes.inlineIcon} /> ), }} diff --git a/app/javascript/mastodon/components/status_list.jsx b/app/javascript/mastodon/components/status_list.jsx index 78e6fbcf5ff214..049905dc085b12 100644 --- a/app/javascript/mastodon/components/status_list.jsx +++ b/app/javascript/mastodon/components/status_list.jsx @@ -8,8 +8,6 @@ import { debounce } from 'lodash'; import { TIMELINE_GAP, TIMELINE_SUGGESTIONS } from 'mastodon/actions/timelines'; import { RegenerationIndicator } from 'mastodon/components/regeneration_indicator'; import { InlineFollowSuggestions } from 'mastodon/features/home_timeline/components/inline_follow_suggestions'; -import { AnnualReportTimeline } from 'mastodon/features/annual_report/timeline'; -import { TIMELINE_WRAPSTODON } from '@/mastodon/reducers/slices/annual_report'; import { StatusQuoteManager } from '../components/status_quoted'; @@ -67,10 +65,6 @@ export default class StatusList extends ImmutablePureComponent { return ( ); - case TIMELINE_WRAPSTODON: - return ( - - ) case TIMELINE_GAP: return ( ; + }, + args: { + id: '1', + }, + parameters: { + state: { + accounts: { + '1': accountFactoryState({ id: '1', acct: 'hashtaguser' }), + }, + statuses: { + '1': statusFactoryState({ + id: '1', + text: 'Hello world!', + }), + }, + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/app/javascript/mastodon/components/status_quoted.tsx b/app/javascript/mastodon/components/status_quoted.tsx index eac2167294f9e9..33e791a548bfa6 100644 --- a/app/javascript/mastodon/components/status_quoted.tsx +++ b/app/javascript/mastodon/components/status_quoted.tsx @@ -4,20 +4,19 @@ import { FormattedMessage } from 'react-intl'; import type { Map as ImmutableMap } from 'immutable'; +import { fetchRelationships } from 'mastodon/actions/accounts'; +import { revealAccount } from 'mastodon/actions/accounts_typed'; +import { fetchStatus } from 'mastodon/actions/statuses'; import { LearnMoreLink } from 'mastodon/components/learn_more_link'; import StatusContainer from 'mastodon/containers/status_container'; import { domain } from 'mastodon/initial_state'; import type { Account } from 'mastodon/models/account'; import type { Status } from 'mastodon/models/status'; +import { makeGetStatusWithExtraInfo } from 'mastodon/selectors'; +import { getAccountHidden } from 'mastodon/selectors/accounts'; import type { RootState } from 'mastodon/store'; import { useAppDispatch, useAppSelector } from 'mastodon/store'; -import { fetchRelationships } from '../actions/accounts'; -import { revealAccount } from '../actions/accounts_typed'; -import { fetchStatus } from '../actions/statuses'; -import { makeGetStatusWithExtraInfo } from '../selectors'; -import { getAccountHidden } from '../selectors/accounts'; - import { Button } from './button'; const MAX_QUOTE_POSTS_NESTING_LEVEL = 1; @@ -333,7 +332,7 @@ export const QuotedStatus: React.FC = ({ ); }; -interface StatusQuoteManagerProps { +export interface StatusQuoteManagerProps { id: string; contextType?: string; [key: string]: unknown; diff --git a/app/javascript/mastodon/features/annual_report/announcement/announcement.stories.tsx b/app/javascript/mastodon/features/annual_report/announcement/announcement.stories.tsx index 926e28bcf2c89d..1b96f6f60bba2c 100644 --- a/app/javascript/mastodon/features/annual_report/announcement/announcement.stories.tsx +++ b/app/javascript/mastodon/features/annual_report/announcement/announcement.stories.tsx @@ -1,38 +1,60 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; -import { fn } from 'storybook/test'; +import { action } from 'storybook/actions'; +import type { AnyFunction, OmitValueType } from '@/mastodon/utils/types'; + +import type { AnnualReportAnnouncementProps } from '.'; import { AnnualReportAnnouncement } from '.'; +type Props = OmitValueType< + // We can't use the name 'state' here because it's reserved for overriding Redux state. + Omit & { + reportState: AnnualReportAnnouncementProps['state']; + }, + AnyFunction // Remove any functions, as they can't meaningfully be controlled in Storybook. +>; + const meta = { - title: 'Components/AnnualReportAnnouncement', - component: AnnualReportAnnouncement, + title: 'Components/AnnualReport/Announcement', args: { - hasData: false, - isLoading: false, + reportState: 'eligible', year: '2025', - onRequestBuild: fn(), - onOpen: fn(), }, -} satisfies Meta; + argTypes: { + reportState: { + control: { + type: 'select', + }, + options: ['eligible', 'generating', 'available'], + }, + }, + render({ reportState, ...args }: Props) { + return ( + + ); + }, +} satisfies Meta; export default meta; type Story = StoryObj; -export const Default: Story = { - render: (args) => , -}; +export const Default: Story = {}; export const Loading: Story = { args: { - isLoading: true, + reportState: 'generating', }, - render: Default.render, }; export const WithData: Story = { args: { - hasData: true, + reportState: 'available', }, - render: Default.render, }; diff --git a/app/javascript/mastodon/features/annual_report/announcement/index.tsx b/app/javascript/mastodon/features/annual_report/announcement/index.tsx index 7cdb36e35f1f10..283e95f594054c 100644 --- a/app/javascript/mastodon/features/annual_report/announcement/index.tsx +++ b/app/javascript/mastodon/features/annual_report/announcement/index.tsx @@ -1,32 +1,37 @@ import { FormattedMessage } from 'react-intl'; +import classNames from 'classnames'; + +import type { ApiAnnualReportState } from '@/mastodon/api/annual_report'; import { Button } from '@/mastodon/components/button'; import styles from './styles.module.scss'; -export const AnnualReportAnnouncement: React.FC<{ +export interface AnnualReportAnnouncementProps { year: string; - hasData: boolean; - isLoading: boolean; + state: Exclude; onRequestBuild: () => void; - onOpen: () => void; -}> = ({ year, hasData, isLoading, onRequestBuild, onOpen }) => { + onOpen?: () => void; // This is optional when inside the modal, as it won't be shown then. + onDismiss: () => void; +} + +export const AnnualReportAnnouncement: React.FC< + AnnualReportAnnouncementProps +> = ({ year, state, onRequestBuild, onOpen, onDismiss }) => { return ( -
-

- -

-

- -

- {hasData ? ( +
+ + + {state === 'available' ? ( ) : ( - )} + {state === 'eligible' && ( + + )}
); }; diff --git a/app/javascript/mastodon/features/annual_report/announcement/styles.module.scss b/app/javascript/mastodon/features/annual_report/announcement/styles.module.scss index a09c3425b649a7..9ec62fa0fdbc0e 100644 --- a/app/javascript/mastodon/features/annual_report/announcement/styles.module.scss +++ b/app/javascript/mastodon/features/annual_report/announcement/styles.module.scss @@ -6,15 +6,17 @@ text-align: center; font-size: 15px; line-height: 1.5; - color: var(--color-text-on-media); - background: var(--color-bg-media-base); + color: var(--color-text-primary); + background: var(--color-bg-primary); background: radial-gradient(at 40% 87%, #240c9a99 0, transparent 50%), radial-gradient(at 19% 10%, #6b0c9a99 0, transparent 50%), radial-gradient(at 90% 27%, #9a0c8299 0, transparent 50%), radial-gradient(at 16% 95%, #1e948299 0, transparent 50%) - var(--color-bg-media-base); + var(--color-bg-primary); border-bottom: 1px solid var(--color-border-primary); + position: relative; + pointer-events: all; h2 { font-size: 20px; @@ -26,4 +28,15 @@ p { margin-bottom: 20px; } + + .closeButton { + position: absolute; + bottom: 8px; + right: 8px; + margin-inline: 0; + } + + :global(.modal-root__modal) & { + border-radius: 16px; + } } diff --git a/app/javascript/mastodon/features/annual_report/annual_report.stories.tsx b/app/javascript/mastodon/features/annual_report/annual_report.stories.tsx new file mode 100644 index 00000000000000..3ccaceae6c5fd6 --- /dev/null +++ b/app/javascript/mastodon/features/annual_report/annual_report.stories.tsx @@ -0,0 +1,99 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { + accountFactoryState, + annualReportFactory, + statusFactoryState, +} from '@/testing/factories'; + +import { AnnualReport } from '.'; + +const SAMPLE_HASHTAG = { + name: 'Mastodon', + count: 14, +}; + +const meta = { + title: 'Components/AnnualReport', + component: AnnualReport, + args: { + context: 'standalone', + }, + parameters: { + state: { + accounts: { + '1': accountFactoryState({ display_name: 'Freddie Fruitbat' }), + }, + statuses: { + '1': statusFactoryState(), + }, + annualReport: annualReportFactory({ + top_hashtag: SAMPLE_HASHTAG, + }), + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Standalone: Story = { + args: { + context: 'standalone', + }, +}; + +export const InModal: Story = { + args: { + context: 'modal', + }, +}; + +export const ArchetypeOracle: Story = { + ...InModal, + parameters: { + state: { + annualReport: annualReportFactory({ + archetype: 'oracle', + top_hashtag: SAMPLE_HASHTAG, + }), + }, + }, +}; + +export const NoHashtag: Story = { + ...InModal, + parameters: { + state: { + annualReport: annualReportFactory({ + archetype: 'booster', + }), + }, + }, +}; + +export const NoNewPosts: Story = { + ...InModal, + parameters: { + state: { + annualReport: annualReportFactory({ + archetype: 'pollster', + top_hashtag: SAMPLE_HASHTAG, + without_posts: true, + }), + }, + }, +}; + +export const NoNewPostsNoHashtag: Story = { + ...InModal, + parameters: { + state: { + annualReport: annualReportFactory({ + archetype: 'replier', + without_posts: true, + }), + }, + }, +}; diff --git a/app/javascript/mastodon/features/annual_report/archetype.tsx b/app/javascript/mastodon/features/annual_report/archetype.tsx index 0c416c30c290d5..7d1cf0bdd48081 100644 --- a/app/javascript/mastodon/features/annual_report/archetype.tsx +++ b/app/javascript/mastodon/features/annual_report/archetype.tsx @@ -1,65 +1,224 @@ -import { defineMessages, useIntl } from 'react-intl'; +import { useCallback, useRef, useState } from 'react'; + +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; + +import classNames from 'classnames'; import booster from '@/images/archetypes/booster.png'; import lurker from '@/images/archetypes/lurker.png'; import oracle from '@/images/archetypes/oracle.png'; import pollster from '@/images/archetypes/pollster.png'; import replier from '@/images/archetypes/replier.png'; -import type { Archetype as ArchetypeData } from '@/mastodon/models/annual_report'; +import space_elements from '@/images/archetypes/space_elements.png'; +import { Avatar } from '@/mastodon/components/avatar'; +import { Button } from '@/mastodon/components/button'; +import { DisplayName } from '@/mastodon/components/display_name'; +import { me } from '@/mastodon/initial_state'; +import type { Account } from '@/mastodon/models/account'; +import type { + AnnualReport, + Archetype as ArchetypeData, +} from '@/mastodon/models/annual_report'; +import { wrapstodonSettings } from '@/mastodon/settings'; + +import styles from './index.module.scss'; +import { ShareButton } from './share_button'; export const archetypeNames = defineMessages({ booster: { - id: 'annual_report.summary.archetype.booster', - defaultMessage: 'The cool-hunter', + id: 'annual_report.summary.archetype.booster.name', + defaultMessage: 'The Archer', + }, + replier: { + id: 'annual_report.summary.archetype.replier.name', + defaultMessage: 'The Butterfly', + }, + pollster: { + id: 'annual_report.summary.archetype.pollster.name', + defaultMessage: 'The Wonderer', + }, + lurker: { + id: 'annual_report.summary.archetype.lurker.name', + defaultMessage: 'The Stoic', + }, + oracle: { + id: 'annual_report.summary.archetype.oracle.name', + defaultMessage: 'The Oracle', + }, +}); + +export const archetypeSelfDescriptions = defineMessages({ + booster: { + id: 'annual_report.summary.archetype.booster.desc_self', + defaultMessage: + 'You stayed on the hunt for posts to boost, amplifying other creators with perfect aim.', }, replier: { - id: 'annual_report.summary.archetype.replier', - defaultMessage: 'The social butterfly', + id: 'annual_report.summary.archetype.replier.desc_self', + defaultMessage: + 'You frequently replied to other people’s posts, pollinating Mastodon with new discussions.', }, pollster: { - id: 'annual_report.summary.archetype.pollster', - defaultMessage: 'The pollster', + id: 'annual_report.summary.archetype.pollster.desc_self', + defaultMessage: + 'You created more polls than other post types, cultivating curiosity on Mastodon.', }, lurker: { - id: 'annual_report.summary.archetype.lurker', - defaultMessage: 'The lurker', + id: 'annual_report.summary.archetype.lurker.desc_self', + defaultMessage: + 'We know you were out there, somewhere, enjoying Mastodon in your own quiet way.', }, oracle: { - id: 'annual_report.summary.archetype.oracle', - defaultMessage: 'The oracle', + id: 'annual_report.summary.archetype.oracle.desc_self', + defaultMessage: + 'You created new posts more than replies, keeping Mastodon fresh and future-facing.', }, }); +export const archetypePublicDescriptions = defineMessages({ + booster: { + id: 'annual_report.summary.archetype.booster.desc_public', + defaultMessage: + '{name} stayed on the hunt for posts to boost, amplifying other creators with perfect aim.', + }, + replier: { + id: 'annual_report.summary.archetype.replier.desc_public', + defaultMessage: + '{name} frequently replied to other people’s posts, pollinating Mastodon with new discussions.', + }, + pollster: { + id: 'annual_report.summary.archetype.pollster.desc_public', + defaultMessage: + '{name} created more polls than other post types, cultivating curiosity on Mastodon.', + }, + lurker: { + id: 'annual_report.summary.archetype.lurker.desc_public', + defaultMessage: + 'We know {name} was out there, somewhere, enjoying Mastodon in their own quiet way.', + }, + oracle: { + id: 'annual_report.summary.archetype.oracle.desc_public', + defaultMessage: + '{name} created new posts more than replies, keeping Mastodon fresh and future-facing.', + }, +}); + +const illustrations = { + booster, + replier, + pollster, + lurker, + oracle, +} as const; + export const Archetype: React.FC<{ - data: ArchetypeData; -}> = ({ data }) => { + report: AnnualReport; + account?: Account; + context: 'modal' | 'standalone'; +}> = ({ report, account, context }) => { const intl = useIntl(); - let illustration; + const wrapperRef = useRef(null); + const isSelfView = context === 'modal'; - switch (data) { - case 'booster': - illustration = booster; - break; - case 'replier': - illustration = replier; - break; - case 'pollster': - illustration = pollster; - break; - case 'lurker': - illustration = lurker; - break; - case 'oracle': - illustration = oracle; - break; - } + const [isRevealed, setIsRevealed] = useState( + () => + !isSelfView || + (me ? (wrapstodonSettings.get(me)?.archetypeRevealed ?? false) : true), + ); + const reveal = useCallback(() => { + setIsRevealed(true); + if (me) { + wrapstodonSettings.set(me, { archetypeRevealed: true }); + } + wrapperRef.current?.focus(); + }, []); + + const archetype = report.data.archetype; + const descriptions = isSelfView + ? archetypeSelfDescriptions + : archetypePublicDescriptions; return ( -
-
- {intl.formatMessage(archetypeNames[data])} +
+
+ {account && ( + + )} +
+ +
+ +
+
+

+ {isSelfView ? ( + + ) : ( + , + }} + /> + )} +

+

+ {isRevealed ? ( + intl.formatMessage(archetypeNames[archetype]) + ) : ( + + )} +

+

+ {isRevealed ? ( + intl.formatMessage(descriptions[archetype], { + name: , + }) + ) : ( + + )} +

- + {!isRevealed && ( + + )} + {isRevealed && isSelfView && }
); }; diff --git a/app/javascript/mastodon/features/annual_report/followers.tsx b/app/javascript/mastodon/features/annual_report/followers.tsx index 196013ae9d6b0c..b0f2216bc5bb65 100644 --- a/app/javascript/mastodon/features/annual_report/followers.tsx +++ b/app/javascript/mastodon/features/annual_report/followers.tsx @@ -1,68 +1,24 @@ import { FormattedMessage, FormattedNumber } from 'react-intl'; -import { Sparklines, SparklinesCurve } from 'react-sparklines'; +import classNames from 'classnames'; -import { ShortNumber } from 'mastodon/components/short_number'; -import type { TimeSeriesMonth } from 'mastodon/models/annual_report'; +import styles from './index.module.scss'; export const Followers: React.FC<{ - data: TimeSeriesMonth[]; - total?: number; -}> = ({ data, total }) => { - const change = data.reduce((sum, item) => sum + item.followers, 0); - - const cumulativeGraph = data.reduce( - (newData, item) => [ - ...newData, - item.followers + (newData[newData.length - 1] ?? 0), - ], - [0], - ); - + count: number; +}> = ({ count }) => { return ( -
- - - - - - - - - - - - - -
-
- {change > -1 ? '+' : '-'} - -
+
+
+ +
-
- - - -
- }} - /> -
-
+
+
); diff --git a/app/javascript/mastodon/features/annual_report/highlighted_post.tsx b/app/javascript/mastodon/features/annual_report/highlighted_post.tsx index 7edbb2e614ff54..d6ca3d3f49d417 100644 --- a/app/javascript/mastodon/features/annual_report/highlighted_post.tsx +++ b/app/javascript/mastodon/features/annual_report/highlighted_post.tsx @@ -1,102 +1,102 @@ /* eslint-disable @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any, - @typescript-eslint/no-unsafe-assignment */ + @typescript-eslint/no-unsafe-assignment, + @typescript-eslint/no-unsafe-member-access, + @typescript-eslint/no-unsafe-call */ +import type { ComponentPropsWithoutRef } from 'react'; import { useCallback } from 'react'; import { FormattedMessage } from 'react-intl'; -import { DisplayName } from '@/mastodon/components/display_name'; -import { toggleStatusSpoilers } from 'mastodon/actions/statuses'; -import { DetailedStatus } from 'mastodon/features/status/components/detailed_status'; -import { me } from 'mastodon/initial_state'; +import classNames from 'classnames'; + +import { InterceptStatusClicks } from 'mastodon/components/status/intercept_status_clicks'; +import { StatusQuoteManager } from 'mastodon/components/status_quoted'; import type { TopStatuses } from 'mastodon/models/annual_report'; -import { makeGetStatus, makeGetPictureInPicture } from 'mastodon/selectors'; -import { useAppSelector, useAppDispatch } from 'mastodon/store'; +import { makeGetStatus } from 'mastodon/selectors'; +import { useAppSelector } from 'mastodon/store'; + +import styles from './index.module.scss'; const getStatus = makeGetStatus() as unknown as (arg0: any, arg1: any) => any; -const getPictureInPicture = makeGetPictureInPicture() as unknown as ( - arg0: any, - arg1: any, -) => any; export const HighlightedPost: React.FC<{ data: TopStatuses; -}> = ({ data }) => { - let statusId, label; + context: 'modal' | 'standalone'; +}> = ({ data, context }) => { + const { by_reblogs, by_favourites, by_replies } = data; + + const statusId = by_reblogs || by_favourites || by_replies; + + const status = useAppSelector((state) => + statusId ? getStatus(state, { id: statusId }) : undefined, + ); - if (data.by_reblogs) { - statusId = data.by_reblogs; + const handleClick = useCallback< + ComponentPropsWithoutRef['onPreventedClick'] + >( + (clickedArea) => { + const link: string = + clickedArea === 'account' + ? status.getIn(['account', 'url']) + : status.get('url'); + + if (context === 'standalone') { + window.location.href = link; + } else { + window.open(link, '_blank'); + } + }, + [status, context], + ); + + if (!status) { + return
; + } + + let label; + if (by_reblogs) { label = ( ); - } else if (data.by_favourites) { - statusId = data.by_favourites; + } else if (by_favourites) { label = ( ); } else { - statusId = data.by_replies; label = ( ); } - const dispatch = useAppDispatch(); - const domain = useAppSelector((state) => state.meta.get('domain')); - const status = useAppSelector((state) => - statusId ? getStatus(state, { id: statusId }) : undefined, - ); - const pictureInPicture = useAppSelector((state) => - statusId ? getPictureInPicture(state, { id: statusId }) : undefined, - ); - const account = useAppSelector((state) => - me ? state.accounts.get(me) : undefined, - ); - - const handleToggleHidden = useCallback(() => { - dispatch(toggleStatusSpoilers(statusId)); - }, [dispatch, statusId]); - - if (!status) { - return ( -
- ); - } - - const displayName = ( - - - , - }} - /> - - {label} - - ); - return ( -
- +
+
+

+ +

+ {context === 'modal' &&

{label}

} +
+ + + +
); }; diff --git a/app/javascript/mastodon/features/annual_report/index.module.scss b/app/javascript/mastodon/features/annual_report/index.module.scss new file mode 100644 index 00000000000000..024518d72f75fe --- /dev/null +++ b/app/javascript/mastodon/features/annual_report/index.module.scss @@ -0,0 +1,351 @@ +$mobile-breakpoint: 540px; + +@font-face { + font-family: silkscreen-wrapstodon; + src: url('@/fonts/silkscreen-wrapstodon/silkscreen-regular.woff2') + format('woff2'); + font-weight: normal; + font-display: swap; + font-style: normal; +} + +.modalWrapper { + box-sizing: border-box; + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + padding: 40px; + overflow-y: auto; + scrollbar-color: var(--color-text-secondary) var(--color-bg-secondary); + + @media (width < $mobile-breakpoint) { + padding: 0; + } + + .loading-indicator .circular-progress { + color: var(--lime); + } +} + +.closeButton { + --default-icon-color: var(--color-bg-primary); + --default-bg-color: var(--color-text-primary); + --hover-icon-color: var(--color-bg-primary); + --hover-bg-color: var(--color-text-primary); + --corner-distance: 18px; + + position: absolute; + top: var(--corner-distance); + right: var(--corner-distance); + padding: 8px; + border-radius: 100%; + + @media (width < $mobile-breakpoint) { + --corner-distance: 16px; + + padding: 4px; + } +} + +.wrapper { + --gradient-strength: 0.4; + + box-sizing: border-box; + position: relative; + max-width: 600px; + padding: 24px; + padding-top: 40px; + contain: layout; + flex: 0 0 auto; + pointer-events: all; + color: var(--color-text-primary); + background: var(--color-bg-primary); + background: + radial-gradient( + at 10% 27%, + rgba(83, 12, 154, var(--gradient-strength)) 0, + transparent 50% + ), + radial-gradient( + at 91% 10%, + rgba(30, 24, 223, var(--gradient-strength)) 0, + transparent 25% + ), + radial-gradient( + at 10% 91%, + rgba(22, 218, 228, var(--gradient-strength)) 0, + transparent 40% + ), + radial-gradient( + at 75% 87%, + rgba(37, 31, 217, var(--gradient-strength)) 0, + transparent 20% + ), + radial-gradient( + at 84% 60%, + rgba(95, 30, 148, var(--gradient-strength)) 0, + transparent 40% + ) + var(--color-bg-primary); + border-radius: 40px; + + @media (width < $mobile-breakpoint) { + padding-inline: 12px; + padding-bottom: 12px; + border-radius: 0; + } +} + +.header { + margin-bottom: 18px; + text-align: center; + + h1 { + font-family: silkscreen-wrapstodon, monospace; + font-size: 28px; + line-height: 1; + margin-bottom: 4px; + padding-inline: 40px; // Prevent overlap with close button + + @media (width < $mobile-breakpoint) { + font-size: 22px; + margin-bottom: 4px; + } + } + + p { + font-size: 14px; + line-height: 1.5; + } +} + +.stack { + --grid-spacing: 12px; + + display: grid; + gap: var(--grid-spacing); +} + +.box { + position: relative; + padding: 24px; + border-radius: 16px; + background: rgb(from var(--color-bg-primary) r g b / 60%); + box-shadow: inset 0 0 0 1px rgb(from var(--color-text-primary) r g b / 40%); + + &::before, + &::after { + content: ''; + position: absolute; + inset-inline: 0; + display: block; + height: 1px; + background-image: linear-gradient( + to right, + transparent, + white, + transparent + ); + } + + &::before { + top: 0; + } + + &::after { + bottom: 0; + } +} + +.content { + display: flex; + flex-direction: column; + justify-content: center; + gap: 8px; + font-size: 14px; + text-align: center; + text-wrap: balance; + + &.comfortable { + gap: 12px; + } +} + +.title { + text-transform: uppercase; + color: var(--color-text-brand-soft); + font-weight: 500; + + &:last-child { + margin-bottom: -3px; + } +} + +.statLarge { + font-size: 24px; + font-weight: 500; + overflow-wrap: break-word; +} + +.statExtraLarge { + font-size: 32px; + font-weight: 500; + line-height: 1; + overflow-wrap: break-word; + + @media (width < $mobile-breakpoint) { + font-size: 24px; + } +} + +.mostBoostedPost { + padding: 0; + padding-top: 24px; + overflow: hidden; +} + +.statsGrid { + display: grid; + gap: var(--grid-spacing); + grid-template-columns: 1fr 2fr; + grid-template-areas: + 'followers hashtag' + 'new-posts hashtag'; + + @media (width < $mobile-breakpoint) { + grid-template-columns: 1fr 1fr; + grid-template-areas: + 'followers new-posts' + 'hashtag hashtag'; + } + + &:empty { + display: none; + } + + &.onlyHashtag { + grid-template-columns: 1fr; + grid-template-areas: 'hashtag'; + } + + &.noHashtag { + grid-template-columns: 1fr 1fr; + grid-template-areas: 'followers new-posts'; + } + + &.singleNumber { + grid-template-columns: 1fr 2fr; + grid-template-areas: 'number hashtag'; + + @media (width < $mobile-breakpoint) { + grid-template-areas: + 'number number' + 'hashtag hashtag'; + } + } + + &.singleNumber.noHashtag { + grid-template-columns: 1fr; + grid-template-areas: 'number'; + } +} + +.followers { + grid-area: followers; + + .singleNumber & { + grid-area: number; + } +} + +.newPosts { + grid-area: new-posts; + + .singleNumber & { + grid-area: number; + } +} + +.mostUsedHashtag { + grid-area: hashtag; + padding-block: 24px; +} + +.archetype { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + + p { + max-width: 460px; + } +} + +.archetypeArtboard { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + align-self: center; + width: 180px; + padding-top: 40px; +} + +.archetypeAvatar { + position: absolute; + top: 7px; + left: 4px; + border-radius: 100%; + overflow: hidden; +} + +.archetypeIllustrationWrapper { + position: relative; + width: 92px; + aspect-ratio: 1; + overflow: hidden; + border-radius: 100%; + + &::after { + content: ''; + display: block; + position: absolute; + inset: 0; + border-radius: inherit; + box-shadow: inset -10px -4px 15px #00000080; + } +} + +.archetypeIllustration { + width: 100%; +} + +.blurredImage { + filter: blur(10px); +} + +.archetypePlanetRing { + position: absolute; + top: 0; + left: 0; + mix-blend-mode: screen; +} + +.shareButtonWrapper { + display: flex; + flex-direction: column; + gap: 10px; +} + +.secondaryShareButton { + // Extra selector is needed to override color + &:global(.button) { + color: var(--color-text-primary); + } +} + +.navItemBadge { + background: var(--color-bg-brand-soft); +} diff --git a/app/javascript/mastodon/features/annual_report/index.tsx b/app/javascript/mastodon/features/annual_report/index.tsx index e9f0b5f2d7e9e7..ef6f73fff28605 100644 --- a/app/javascript/mastodon/features/annual_report/index.tsx +++ b/app/javascript/mastodon/features/annual_report/index.tsx @@ -1,95 +1,125 @@ -import { useCallback } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import type { FC } from 'react'; -import { defineMessage, FormattedMessage, useIntl } from 'react-intl'; +import { useIntl } from 'react-intl'; + +import { useLocation } from 'react-router'; + +import classNames from 'classnames/bind'; -import { focusCompose, resetCompose } from '@/mastodon/actions/compose'; import { closeModal } from '@/mastodon/actions/modal'; -import { Button } from '@/mastodon/components/button'; +import { IconButton } from '@/mastodon/components/icon_button'; import { LoadingIndicator } from '@/mastodon/components/loading_indicator'; -import { me } from '@/mastodon/initial_state'; -import type { AnnualReport as AnnualReportData } from '@/mastodon/models/annual_report'; -import { useAppDispatch, useAppSelector } from '@/mastodon/store'; +import { getReport } from '@/mastodon/reducers/slices/annual_report'; +import { + createAppSelector, + useAppDispatch, + useAppSelector, +} from '@/mastodon/store'; +import CloseIcon from '@/material-icons/400-24px/close.svg?react'; -import { Archetype, archetypeNames } from './archetype'; +import { Archetype } from './archetype'; import { Followers } from './followers'; import { HighlightedPost } from './highlighted_post'; +import styles from './index.module.scss'; import { MostUsedHashtag } from './most_used_hashtag'; import { NewPosts } from './new_posts'; -const shareMessage = defineMessage({ - id: 'annual_report.summary.share_message', - defaultMessage: 'I got the {archetype} archetype!', -}); +const moduleClassNames = classNames.bind(styles); -// Share = false when using the embedded version of the report. -export const AnnualReport: FC<{ share?: boolean }> = ({ share = true }) => { - const currentAccount = useAppSelector((state) => - me ? state.accounts.get(me) : undefined, - ); +export const accountSelector = createAppSelector( + [(state) => state.accounts, (state) => state.annualReport.report], + (accounts, report) => { + if (report?.schema_version === 2) { + return accounts.get(report.account_id); + } + return undefined; + }, +); + +export const AnnualReport: FC<{ context?: 'modal' | 'standalone' }> = ({ + context = 'standalone', +}) => { + const intl = useIntl(); + const dispatch = useAppDispatch(); const report = useAppSelector((state) => state.annualReport.report); + const account = useAppSelector(accountSelector); + const needsReport = !report; // Make into boolean to avoid object comparison in deps. + + useEffect(() => { + if (needsReport) { + void dispatch(getReport()); + } + }, [dispatch, needsReport]); + + const close = useCallback(() => { + dispatch(closeModal({ modalType: 'ANNUAL_REPORT', ignoreFocus: false })); + }, [dispatch]); + + // Close modal when navigating away from within + const { pathname } = useLocation(); + const [initialPathname] = useState(pathname); + useEffect(() => { + if (pathname !== initialPathname) { + close(); + } + }, [pathname, initialPathname, close]); - if (!report) { + if (needsReport) { return ; } + const newPostCount = report.data.time_series.reduce( + (sum, item) => sum + item.statuses, + 0, + ); + + const newFollowerCount = + context === 'modal' && + report.data.time_series.reduce((sum, item) => sum + item.followers, 0); + + const topHashtag = report.data.top_hashtags[0]; + return ( -
-
-

- +
+

Wrapstodon {report.year}

+ {account &&

@{account.acct}

} + {context === 'modal' && ( + -

-

- -

+ )}
-
- - - - - - {share && } +
+ +
+ {!!newFollowerCount && } + {!!newPostCount && } + {topHashtag && ( + + )} +
+
); }; - -const ShareButton: FC<{ report: AnnualReportData }> = ({ report }) => { - const intl = useIntl(); - const dispatch = useAppDispatch(); - const handleShareClick = useCallback(() => { - // Generate the share message. - const archetypeName = intl.formatMessage( - archetypeNames[report.data.archetype], - ); - const shareLines = [ - intl.formatMessage(shareMessage, { - archetype: archetypeName, - }), - ]; - // Share URL is only available for schema version 2. - if (report.schema_version === 2 && report.share_url) { - shareLines.push(report.share_url); - } - shareLines.push(`#Wrapstodon${report.year}`); - - // Reset the composer and focus it with the share message, then close the modal. - dispatch(resetCompose()); - dispatch(focusCompose(shareLines.join('\n\n'))); - dispatch(closeModal({ modalType: 'ANNUAL_REPORT', ignoreFocus: false })); - }, [report, intl, dispatch]); - - return + ); +}; diff --git a/app/javascript/mastodon/features/annual_report/new_posts.tsx b/app/javascript/mastodon/features/annual_report/new_posts.tsx index 9ead0176b2ad7d..9a265f0b9d47a2 100644 --- a/app/javascript/mastodon/features/annual_report/new_posts.tsx +++ b/app/javascript/mastodon/features/annual_report/new_posts.tsx @@ -1,51 +1,23 @@ import { FormattedNumber, FormattedMessage } from 'react-intl'; -import ChatBubbleIcon from '@/material-icons/400-24px/chat_bubble.svg?react'; -import type { TimeSeriesMonth } from 'mastodon/models/annual_report'; +import classNames from 'classnames'; -export const NewPosts: React.FC<{ - data: TimeSeriesMonth[]; -}> = ({ data }) => { - const posts = data.reduce((sum, item) => sum + item.statuses, 0); +import styles from './index.module.scss'; +export const NewPosts: React.FC<{ + count: number; +}> = ({ count }) => { return ( -
- - - - - - - - - - - -
- +
+
+
-
+ +
diff --git a/app/javascript/mastodon/features/annual_report/share.module.css b/app/javascript/mastodon/features/annual_report/share.module.css deleted file mode 100644 index e4d01ff3e33cb2..00000000000000 --- a/app/javascript/mastodon/features/annual_report/share.module.css +++ /dev/null @@ -1,19 +0,0 @@ -.wrapper { - max-width: 40rem; - margin: 0 auto; -} - -.footer { - text-align: center; - margin-top: 1rem; - display: flex; - flex-direction: column; - gap: 0.75rem; - align-items: center; - color: var(--color-text-secondary); -} - -.logo { - width: 2rem; - opacity: 0.6; -} diff --git a/app/javascript/mastodon/features/annual_report/share.tsx b/app/javascript/mastodon/features/annual_report/share.tsx deleted file mode 100644 index 25b3ee9885c643..00000000000000 --- a/app/javascript/mastodon/features/annual_report/share.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type { FC } from 'react'; - -import { IconLogo } from '@/mastodon/components/logo'; - -import { AnnualReport } from './index'; -import classes from './share.module.css'; - -export const WrapstodonShare: FC = () => { - return ( -
- -
- - Generated with ♥ by the Mastodon team -
-
- ); -}; diff --git a/app/javascript/mastodon/features/annual_report/share_button.tsx b/app/javascript/mastodon/features/annual_report/share_button.tsx new file mode 100644 index 00000000000000..16dd834f4a17e0 --- /dev/null +++ b/app/javascript/mastodon/features/annual_report/share_button.tsx @@ -0,0 +1,96 @@ +import { useCallback } from 'react'; +import type { FC } from 'react'; + +import { defineMessages, useIntl } from 'react-intl'; + +import { showAlert } from '@/mastodon/actions/alerts'; +import { resetCompose, focusCompose } from '@/mastodon/actions/compose'; +import { closeModal } from '@/mastodon/actions/modal'; +import { Button } from '@/mastodon/components/button'; +import type { AnnualReport as AnnualReportData } from '@/mastodon/models/annual_report'; +import { useAppDispatch } from '@/mastodon/store'; + +import { archetypeNames } from './archetype'; +import styles from './index.module.scss'; + +const messages = defineMessages({ + share_message: { + id: 'annual_report.summary.share_message', + defaultMessage: 'I got the {archetype} archetype!', + }, + share_on_mastodon: { + id: 'annual_report.summary.share_on_mastodon', + defaultMessage: 'Share on Mastodon', + }, + share_elsewhere: { + id: 'annual_report.summary.share_elsewhere', + defaultMessage: 'Share elsewhere', + }, + copy_link: { + id: 'annual_report.summary.copy_link', + defaultMessage: 'Copy link', + }, + copied: { + id: 'copy_icon_button.copied', + defaultMessage: 'Copied to clipboard', + }, +}); + +export const ShareButton: FC<{ report: AnnualReportData }> = ({ report }) => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + + const handleShareClick = useCallback(() => { + // Generate the share message. + const archetypeName = intl.formatMessage( + archetypeNames[report.data.archetype], + ); + const shareLines = [ + intl.formatMessage(messages.share_message, { + archetype: archetypeName, + }), + ]; + // Share URL is only available for schema version 2. + if (report.schema_version === 2 && report.share_url) { + shareLines.push(report.share_url); + } + shareLines.push(`#Wrapstodon${report.year}`); + + // Reset the composer and focus it with the share message, then close the modal. + dispatch(resetCompose()); + dispatch(focusCompose(shareLines.join('\n\n'))); + dispatch(closeModal({ modalType: 'ANNUAL_REPORT', ignoreFocus: false })); + }, [report, intl, dispatch]); + + const supportsNativeShare = 'share' in navigator; + + const handleSecondaryShare = useCallback(() => { + if (report.schema_version === 2 && report.share_url) { + if (supportsNativeShare) { + void navigator.share({ + url: report.share_url, + }); + } else { + void navigator.clipboard.writeText(report.share_url); + dispatch(showAlert({ message: messages.copied })); + } + } + }, [report, supportsNativeShare, dispatch]); + + return ( +
+
+ ); +}; diff --git a/app/javascript/mastodon/features/annual_report/shared_page.module.scss b/app/javascript/mastodon/features/annual_report/shared_page.module.scss new file mode 100644 index 00000000000000..ea3ea471b90153 --- /dev/null +++ b/app/javascript/mastodon/features/annual_report/shared_page.module.scss @@ -0,0 +1,58 @@ +$mobile-breakpoint: 540px; + +.wrapper { + box-sizing: border-box; + max-width: 600px; + margin-inline: auto; + padding: 40px 10px; + + @media (width < $mobile-breakpoint) { + padding-top: 0; + padding-inline: 0; + } +} + +.footer { + display: flex; + flex-direction: column; + align-items: center; + gap: 1.8rem; + margin-top: 2rem; + font-size: 16px; + line-height: 1.4; + text-align: center; + color: var(--color-text-secondary); + + strong { + font-weight: 600; + } + + a:any-link { + color: inherit; + text-decoration: underline; + text-underline-offset: 0.2em; + } + + a:hover { + color: var(--color-text-primary); + } +} + +.logo { + width: 2rem; + opacity: 0.6; +} + +.footerSection { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; +} + +.linkList { + list-style: none; + display: flex; + flex-wrap: wrap; + gap: 12px; +} diff --git a/app/javascript/mastodon/features/annual_report/shared_page.tsx b/app/javascript/mastodon/features/annual_report/shared_page.tsx new file mode 100644 index 00000000000000..3defe7194a3ec2 --- /dev/null +++ b/app/javascript/mastodon/features/annual_report/shared_page.tsx @@ -0,0 +1,68 @@ +import type { FC } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { DisplayName } from '@/mastodon/components/display_name'; +import { IconLogo } from '@/mastodon/components/logo'; +import { useAppSelector } from '@/mastodon/store'; + +import { AnnualReport, accountSelector } from './index'; +import classes from './shared_page.module.scss'; + +export const WrapstodonSharedPage: FC = () => { + const account = useAppSelector(accountSelector); + const domain = useAppSelector((state) => state.meta.get('domain') as string); + return ( +
+ +
+
+ + + +
+ +
+ , + domain: {domain}, + }} + tagName='p' + /> + + + +
+
+
+ ); +}; diff --git a/app/javascript/mastodon/features/annual_report/timeline.tsx b/app/javascript/mastodon/features/annual_report/timeline.tsx index ee46d204031dcb..28a4b1d27324ae 100644 --- a/app/javascript/mastodon/features/annual_report/timeline.tsx +++ b/app/javascript/mastodon/features/annual_report/timeline.tsx @@ -2,38 +2,40 @@ import { useCallback } from 'react'; import type { FC } from 'react'; import { openModal } from '@/mastodon/actions/modal'; -import { - generateReport, - selectWrapstodonYear, -} from '@/mastodon/reducers/slices/annual_report'; +import { useDismissible } from '@/mastodon/hooks/useDismissible'; +import { generateReport } from '@/mastodon/reducers/slices/annual_report'; import { useAppDispatch, useAppSelector } from '@/mastodon/store'; import { AnnualReportAnnouncement } from './announcement'; export const AnnualReportTimeline: FC = () => { - const { state } = useAppSelector((state) => state.annualReport); - const year = useAppSelector(selectWrapstodonYear); + const { state, year } = useAppSelector((state) => state.annualReport); const dispatch = useAppDispatch(); const handleBuildRequest = useCallback(() => { void dispatch(generateReport()); }, [dispatch]); + const { wasDismissed, dismiss } = useDismissible( + `annual_report_announcement_${year}`, + ); + const handleOpen = useCallback(() => { dispatch(openModal({ modalType: 'ANNUAL_REPORT', modalProps: {} })); - }, [dispatch]); + dismiss(); + }, [dismiss, dispatch]); - if (!year || !state || state === 'ineligible') { + if (!year || wasDismissed || !state || state === 'ineligible') { return null; } return ( ); }; diff --git a/app/javascript/mastodon/features/audio/index.tsx b/app/javascript/mastodon/features/audio/index.tsx index c16fd9eab15a0d..fa0d976377cc30 100644 --- a/app/javascript/mastodon/features/audio/index.tsx +++ b/app/javascript/mastodon/features/audio/index.tsx @@ -41,8 +41,8 @@ const persistVolume = (volume: number, muted: boolean) => { }; const restoreVolume = (audio: HTMLAudioElement) => { - const volume = (playerSettings.get('volume') as number | undefined) ?? 0.5; - const muted = (playerSettings.get('muted') as boolean | undefined) ?? false; + const volume = playerSettings.get('volume') ?? 0.5; + const muted = playerSettings.get('muted') ?? false; audio.volume = volume; audio.muted = muted; diff --git a/app/javascript/mastodon/features/compose/components/language_dropdown.tsx b/app/javascript/mastodon/features/compose/components/language_dropdown.tsx index 6ad0c367a9f709..8f75c3dd09fdbc 100644 --- a/app/javascript/mastodon/features/compose/components/language_dropdown.tsx +++ b/app/javascript/mastodon/features/compose/components/language_dropdown.tsx @@ -399,9 +399,10 @@ export const LanguageDropdown: React.FC = () => { } return ( -
+ <>
)} -
+ ); }; diff --git a/app/javascript/mastodon/features/compose/components/search.tsx b/app/javascript/mastodon/features/compose/components/search.tsx index f6e525ff12d619..687d8ee00f1124 100644 --- a/app/javascript/mastodon/features/compose/components/search.tsx +++ b/app/javascript/mastodon/features/compose/components/search.tsx @@ -1,4 +1,11 @@ -import { useCallback, useState, useRef, useEffect, useMemo } from 'react'; +import { + useCallback, + useState, + useRef, + useEffect, + useMemo, + useId, +} from 'react'; import { defineMessages, @@ -474,12 +481,17 @@ export const Search: React.FC<{ switch (e.key) { case 'Escape': e.preventDefault(); - unfocus(); + searchInputRef.current?.focus(); + setExpanded(false); break; case 'ArrowDown': e.preventDefault(); + if (!expanded) { + setExpanded(true); + } + if (navigableOptions.length > 0) { setSelectedOption( Math.min(selectedOption + 1, navigableOptions.length - 1), @@ -518,10 +530,10 @@ export const Search: React.FC<{ break; } }, - [unfocus, navigableOptions, selectedOption, submit, value], + [expanded, navigableOptions, selectedOption, submit, value], ); - const handleFocus = useCallback(() => { + const handleInputFocus = useCallback(() => { setExpanded(true); setSelectedOption(-1); @@ -537,10 +549,16 @@ export const Search: React.FC<{ } }, [setExpanded, setSelectedOption, singleColumn]); - const handleBlur = useCallback(() => { + const handleInputBlur = useCallback(() => { setSelectedOption(-1); }, [setSelectedOption]); + const getOptionFocusHandler = useCallback((index: number) => { + return () => { + setSelectedOption(index); + }; + }, []); + const formRef = useRef(null); useEffect(() => { @@ -568,6 +586,8 @@ export const Search: React.FC<{ return () => null; }, [expanded]); + const searchOptionsHeading = useId(); + return (
-
+ {/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */} +
{!hasValue && ( <>

@@ -607,6 +634,7 @@ export const Search: React.FC<{ tabIndex={0} role='button' onMouseDown={action} + onFocus={getOptionFocusHandler(i)} className={classNames( 'search__popout__menu__item search__popout__menu__item--flex', { selected: selectedOption === i }, @@ -648,6 +676,7 @@ export const Search: React.FC<{ - ))} + {searchOptions.map(({ key, label, action }, i) => { + const currentIndex = (quickActions.length || recent.length) + i; + return ( + + ); + })}

) : (
diff --git a/app/javascript/mastodon/features/emoji/constants.ts b/app/javascript/mastodon/features/emoji/constants.ts index f7705731214e8e..e02663c9d84082 100644 --- a/app/javascript/mastodon/features/emoji/constants.ts +++ b/app/javascript/mastodon/features/emoji/constants.ts @@ -23,6 +23,10 @@ export const EMOJI_MODE_TWEMOJI = 'twemoji'; export const EMOJI_TYPE_UNICODE = 'unicode'; export const EMOJI_TYPE_CUSTOM = 'custom'; +export const EMOJI_DB_NAME_SHORTCODES = 'shortcodes'; + +export const EMOJI_DB_SHORTCODE_TEST = '2122'; // 2122 is the trademark sign, which we know has shortcodes in all datasets. + export const EMOJIS_WITH_DARK_BORDER = [ '🎱', // 1F3B1 '🐜', // 1F41C diff --git a/app/javascript/mastodon/features/emoji/database.test.ts b/app/javascript/mastodon/features/emoji/database.test.ts index 0689fd7c542d7f..6b6ea952b74cdc 100644 --- a/app/javascript/mastodon/features/emoji/database.test.ts +++ b/app/javascript/mastodon/features/emoji/database.test.ts @@ -1,7 +1,8 @@ import { IDBFactory } from 'fake-indexeddb'; -import { unicodeEmojiFactory } from '@/testing/factories'; +import { customEmojiFactory, unicodeEmojiFactory } from '@/testing/factories'; +import { EMOJI_DB_SHORTCODE_TEST } from './constants'; import { putEmojiData, loadEmojiByHexcode, @@ -9,6 +10,11 @@ import { searchEmojisByTag, testClear, testGet, + putCustomEmojiData, + putLegacyShortcodes, + loadLegacyShortcodesByShortcode, + loadLatestEtag, + putLatestEtag, } from './database'; describe('emoji database', () => { @@ -16,6 +22,7 @@ describe('emoji database', () => { testClear(); indexedDB = new IDBFactory(); }); + describe('putEmojiData', () => { test('adds to loaded locales', async () => { const { loadedLocales } = await testGet(); @@ -33,6 +40,44 @@ describe('emoji database', () => { }); }); + describe('putCustomEmojiData', () => { + test('loads custom emoji into indexedDB', async () => { + const { db } = await testGet(); + await putCustomEmojiData({ emojis: [customEmojiFactory()] }); + await expect(db.get('custom', 'custom')).resolves.toEqual( + customEmojiFactory(), + ); + }); + + test('clears existing custom emoji if specified', async () => { + const { db } = await testGet(); + await putCustomEmojiData({ + emojis: [customEmojiFactory({ shortcode: 'emoji1' })], + }); + await putCustomEmojiData({ + emojis: [customEmojiFactory({ shortcode: 'emoji2' })], + clear: true, + }); + await expect(db.get('custom', 'emoji1')).resolves.toBeUndefined(); + await expect(db.get('custom', 'emoji2')).resolves.toEqual( + customEmojiFactory({ shortcode: 'emoji2' }), + ); + }); + }); + + describe('putLegacyShortcodes', () => { + test('loads shortcodes into indexedDB', async () => { + const { db } = await testGet(); + await putLegacyShortcodes({ + test_hexcode: ['shortcode1', 'shortcode2'], + }); + await expect(db.get('shortcodes', 'test_hexcode')).resolves.toEqual({ + hexcode: 'test_hexcode', + shortcodes: ['shortcode1', 'shortcode2'], + }); + }); + }); + describe('loadEmojiByHexcode', () => { test('throws if the locale is not loaded', async () => { await expect(loadEmojiByHexcode('en', 'test')).rejects.toThrowError( @@ -136,4 +181,58 @@ describe('emoji database', () => { expect(actual).toHaveLength(0); }); }); + + describe('loadLegacyShortcodesByShortcode', () => { + const data = { + hexcode: 'test_hexcode', + shortcodes: ['shortcode1', 'shortcode2'], + }; + + beforeEach(async () => { + await putLegacyShortcodes({ + [data.hexcode]: data.shortcodes, + }); + }); + + test('retrieves the shortcodes', async () => { + await expect( + loadLegacyShortcodesByShortcode('shortcode1'), + ).resolves.toEqual(data); + await expect( + loadLegacyShortcodesByShortcode('shortcode2'), + ).resolves.toEqual(data); + }); + }); + + describe('loadLatestEtag', () => { + beforeEach(async () => { + await putLatestEtag('etag', 'en'); + await putEmojiData([unicodeEmojiFactory()], 'en'); + await putLatestEtag('fr-etag', 'fr'); + }); + + test('retrieves the etag for loaded locale', async () => { + await putEmojiData( + [unicodeEmojiFactory({ hexcode: EMOJI_DB_SHORTCODE_TEST })], + 'en', + ); + const etag = await loadLatestEtag('en'); + expect(etag).toBe('etag'); + }); + + test('returns null if locale has no shortcodes', async () => { + const etag = await loadLatestEtag('en'); + expect(etag).toBeNull(); + }); + + test('returns null if locale not loaded', async () => { + const etag = await loadLatestEtag('de'); + expect(etag).toBeNull(); + }); + + test('returns null if locale has no data', async () => { + const etag = await loadLatestEtag('fr'); + expect(etag).toBeNull(); + }); + }); }); diff --git a/app/javascript/mastodon/features/emoji/database.ts b/app/javascript/mastodon/features/emoji/database.ts index b7f8a32f76ca83..fe4010a861d994 100644 --- a/app/javascript/mastodon/features/emoji/database.ts +++ b/app/javascript/mastodon/features/emoji/database.ts @@ -1,14 +1,11 @@ import { SUPPORTED_LOCALES } from 'emojibase'; -import type { Locale } from 'emojibase'; +import type { Locale, ShortcodesDataset } from 'emojibase'; import type { DBSchema, IDBPDatabase } from 'idb'; import { openDB } from 'idb'; +import { EMOJI_DB_SHORTCODE_TEST } from './constants'; import { toSupportedLocale, toSupportedLocaleOrCustom } from './locale'; -import type { - CustomEmojiData, - UnicodeEmojiData, - LocaleOrCustom, -} from './types'; +import type { CustomEmojiData, UnicodeEmojiData, EtagTypes } from './types'; import { emojiLogger } from './utils'; interface EmojiDB extends LocaleTables, DBSchema { @@ -19,8 +16,19 @@ interface EmojiDB extends LocaleTables, DBSchema { category: string; }; }; + shortcodes: { + key: string; + value: { + hexcode: string; + shortcodes: string[]; + }; + indexes: { + hexcode: string; + shortcodes: string[]; + }; + }; etags: { - key: LocaleOrCustom; + key: EtagTypes; value: string; }; } @@ -33,13 +41,14 @@ interface LocaleTable { label: string; order: number; tags: string[]; + shortcodes: string[]; }; } type LocaleTables = Record; type Database = IDBPDatabase; -const SCHEMA_VERSION = 1; +const SCHEMA_VERSION = 2; const loadedLocales = new Set(); @@ -52,28 +61,76 @@ const loadDB = (() => { // Actually load the DB. async function initDB() { const db = await openDB('mastodon-emoji', SCHEMA_VERSION, { - upgrade(database) { - const customTable = database.createObjectStore('custom', { - keyPath: 'shortcode', - autoIncrement: false, - }); - customTable.createIndex('category', 'category'); + upgrade(database, oldVersion, newVersion, trx) { + if (!database.objectStoreNames.contains('custom')) { + const customTable = database.createObjectStore('custom', { + keyPath: 'shortcode', + autoIncrement: false, + }); + customTable.createIndex('category', 'category'); + } - database.createObjectStore('etags'); + if (!database.objectStoreNames.contains('etags')) { + database.createObjectStore('etags'); + } for (const locale of SUPPORTED_LOCALES) { - const localeTable = database.createObjectStore(locale, { + if (!database.objectStoreNames.contains(locale)) { + const localeTable = database.createObjectStore(locale, { + keyPath: 'hexcode', + autoIncrement: false, + }); + localeTable.createIndex('group', 'group'); + localeTable.createIndex('label', 'label'); + localeTable.createIndex('order', 'order'); + localeTable.createIndex('tags', 'tags', { multiEntry: true }); + localeTable.createIndex('shortcodes', 'shortcodes', { + multiEntry: true, + }); + } + // Added in version 2. + const localeTable = trx.objectStore(locale); + if (!localeTable.indexNames.contains('shortcodes')) { + localeTable.createIndex('shortcodes', 'shortcodes', { + multiEntry: true, + }); + } + } + + if (!database.objectStoreNames.contains('shortcodes')) { + const shortcodeTable = database.createObjectStore('shortcodes', { keyPath: 'hexcode', autoIncrement: false, }); - localeTable.createIndex('group', 'group'); - localeTable.createIndex('label', 'label'); - localeTable.createIndex('order', 'order'); - localeTable.createIndex('tags', 'tags', { multiEntry: true }); + shortcodeTable.createIndex('hexcode', 'hexcode'); + shortcodeTable.createIndex('shortcodes', 'shortcodes', { + multiEntry: true, + }); } + + log( + 'Upgraded emoji database from version %d to %d', + oldVersion, + newVersion, + ); + }, + blocked(currentVersion, blockedVersion) { + log( + 'Emoji database upgrade from version %d to %d is blocked', + currentVersion, + blockedVersion, + ); + }, + blocking(currentVersion, blockedVersion) { + log( + 'Emoji database upgrade from version %d is blocking upgrade to %d', + currentVersion, + blockedVersion, + ); }, }); await syncLocales(db); + log('Loaded database version %d', db.version); return db; } @@ -100,17 +157,52 @@ export async function putEmojiData(emojis: UnicodeEmojiData[], locale: Locale) { await trx.done; } -export async function putCustomEmojiData(emojis: CustomEmojiData[]) { +export async function putCustomEmojiData({ + emojis, + clear = false, +}: { + emojis: CustomEmojiData[]; + clear?: boolean; +}) { const db = await loadDB(); const trx = db.transaction('custom', 'readwrite'); + + // When importing from the API, clear everything first. + if (clear) { + await trx.store.clear(); + log('Cleared existing custom emojis in database'); + } + await Promise.all(emojis.map((emoji) => trx.store.put(emoji))); await trx.done; + + log('Imported %d custom emojis into database', emojis.length); +} + +export async function putLegacyShortcodes(shortcodes: ShortcodesDataset) { + const db = await loadDB(); + const trx = db.transaction('shortcodes', 'readwrite'); + await Promise.all( + Object.entries(shortcodes).map(([hexcode, codes]) => + trx.store.put({ + hexcode, + shortcodes: Array.isArray(codes) ? codes : [codes], + }), + ), + ); + await trx.done; +} + +export async function putLatestEtag(etag: string, name: EtagTypes) { + const db = await loadDB(); + await db.put('etags', etag, name); } -export async function putLatestEtag(etag: string, localeString: string) { +export async function clearEtag(localeString: string) { const locale = toSupportedLocaleOrCustom(localeString); const db = await loadDB(); - await db.put('etags', etag, locale); + await db.delete('etags', locale); + log('Cleared etag for %s', locale); } export async function loadEmojiByHexcode( @@ -161,6 +253,15 @@ export async function searchCustomEmojisByShortcodes(shortcodes: string[]) { return results.filter((emoji) => shortcodes.includes(emoji.shortcode)); } +export async function loadLegacyShortcodesByShortcode(shortcode: string) { + const db = await loadDB(); + return db.getFromIndex( + 'shortcodes', + 'shortcodes', + IDBKeyRange.only(shortcode), + ); +} + export async function loadLatestEtag(localeString: string) { const locale = toSupportedLocaleOrCustom(localeString); const db = await loadDB(); @@ -168,6 +269,15 @@ export async function loadLatestEtag(localeString: string) { if (!rowCount) { return null; // No data for this locale, return null even if there is an etag. } + + // Check if shortcodes exist for the given Unicode locale. + if (locale !== 'custom') { + const result = await db.get(locale, EMOJI_DB_SHORTCODE_TEST); + if (!result?.shortcodes) { + return null; + } + } + const etag = await db.get('etags', locale); return etag ?? null; } diff --git a/app/javascript/mastodon/features/emoji/index.ts b/app/javascript/mastodon/features/emoji/index.ts index 4b0f79133c5a10..b134d884415e03 100644 --- a/app/javascript/mastodon/features/emoji/index.ts +++ b/app/javascript/mastodon/features/emoji/index.ts @@ -1,10 +1,9 @@ import { initialState } from '@/mastodon/initial_state'; +import type { EMOJI_DB_NAME_SHORTCODES } from './constants'; import { toSupportedLocale } from './locale'; import type { LocaleOrCustom } from './types'; import { emojiLogger } from './utils'; -// eslint-disable-next-line import/default -- Importing via worker loader. -import EmojiWorker from './worker?worker&inline'; const userLocale = toSupportedLocale(initialState?.meta.locale ?? 'en'); @@ -12,66 +11,69 @@ let worker: Worker | null = null; const log = emojiLogger('index'); -const WORKER_TIMEOUT = 1_000; // 1 second +// This is too short, but better to fallback quickly than wait. +const WORKER_TIMEOUT = 1_000; -export function initializeEmoji() { +export async function initializeEmoji() { log('initializing emojis'); + + // Create a temp worker, and assign it to the module-level worker once we know it's ready. + let tempWorker: Worker | null = null; if (!worker && 'Worker' in window) { try { - worker = new EmojiWorker(); + const { default: EmojiWorker } = await import('./worker?worker&inline'); + tempWorker = new EmojiWorker(); } catch (err) { console.warn('Error creating web worker:', err); } } - if (worker) { - const timeoutId = setTimeout(() => { - log('worker is not ready after timeout'); - worker = null; - void fallbackLoad(); - }, WORKER_TIMEOUT); - worker.addEventListener('message', (event: MessageEvent) => { - const { data: message } = event; - if (message === 'ready') { - log('worker ready, loading data'); - clearTimeout(timeoutId); - messageWorker('custom'); - void loadEmojiLocale(userLocale); - // Load English locale as well, because people are still used to - // using it from before we supported other locales. - if (userLocale !== 'en') { - void loadEmojiLocale('en'); - } - } else { - log('got worker message: %s', message); - } - }); - } else { + if (!tempWorker) { void fallbackLoad(); + return; } + + const timeoutId = setTimeout(() => { + log('worker is not ready after timeout'); + void fallbackLoad(); + }, WORKER_TIMEOUT); + + tempWorker.addEventListener('message', (event: MessageEvent) => { + const { data: message } = event; + + worker ??= tempWorker; + + if (message === 'ready') { + log('worker ready, loading data'); + clearTimeout(timeoutId); + messageWorker('shortcodes'); + void loadCustomEmoji(); + void loadEmojiLocale(userLocale); + } else { + log('got worker message: %s', message); + } + }); } async function fallbackLoad() { log('falling back to main thread for loading'); - const { importCustomEmojiData } = await import('./loader'); - const emojis = await importCustomEmojiData(); - if (emojis) { - log('loaded %d custom emojis', emojis.length); + + await loadCustomEmoji(); + const { importLegacyShortcodes } = await import('./loader'); + const shortcodes = await importLegacyShortcodes(); + if (shortcodes?.length) { + log('loaded %d legacy shortcodes', shortcodes.length); } await loadEmojiLocale(userLocale); - if (userLocale !== 'en') { - await loadEmojiLocale('en'); - } } async function loadEmojiLocale(localeString: string) { const locale = toSupportedLocale(localeString); - const { importEmojiData, localeToPath } = await import('./loader'); + const { importEmojiData } = await import('./loader'); if (worker) { - const path = await localeToPath(locale); - log('asking worker to load locale %s from %s', locale, path); - messageWorker(locale, path); + log('asking worker to load locale %s', locale); + messageWorker(locale); } else { const emojis = await importEmojiData(locale); if (emojis) { @@ -80,9 +82,23 @@ async function loadEmojiLocale(localeString: string) { } } -function messageWorker(locale: LocaleOrCustom, path?: string) { +export async function loadCustomEmoji() { + if (worker) { + messageWorker('custom'); + } else { + const { importCustomEmojiData } = await import('./loader'); + const emojis = await importCustomEmojiData(); + if (emojis && emojis.length > 0) { + log('loaded %d custom emojis', emojis.length); + } + } +} + +function messageWorker( + locale: LocaleOrCustom | typeof EMOJI_DB_NAME_SHORTCODES, +) { if (!worker) { return; } - worker.postMessage({ locale, path }); + worker.postMessage({ locale }); } diff --git a/app/javascript/mastodon/features/emoji/loader.ts b/app/javascript/mastodon/features/emoji/loader.ts index 7251559d6b2fa9..c6b64fe29c0d4d 100644 --- a/app/javascript/mastodon/features/emoji/loader.ts +++ b/app/javascript/mastodon/features/emoji/loader.ts @@ -1,73 +1,145 @@ import { flattenEmojiData } from 'emojibase'; -import type { CompactEmoji, FlatCompactEmoji, Locale } from 'emojibase'; +import type { + CompactEmoji, + FlatCompactEmoji, + Locale, + ShortcodesDataset, +} from 'emojibase'; import { putEmojiData, putCustomEmojiData, loadLatestEtag, putLatestEtag, + putLegacyShortcodes, } from './database'; -import { toSupportedLocale, toSupportedLocaleOrCustom } from './locale'; +import { toSupportedLocale, toValidEtagName } from './locale'; import type { CustomEmojiData } from './types'; +import { emojiLogger } from './utils'; -export async function importEmojiData(localeString: string, path?: string) { +const log = emojiLogger('loader'); + +export async function importEmojiData(localeString: string, shortcodes = true) { const locale = toSupportedLocale(localeString); - // Validate the provided path. - if (path && !/^[/a-z]*\/packs\/assets\/compact-\w+\.json$/.test(path)) { - throw new Error('Invalid path for emoji data'); - } else { - // Otherwise get the path if not provided. - path ??= await localeToPath(locale); - } + log( + 'importing emoji data for locale %s%s', + locale, + shortcodes ? ' and shortcodes' : '', + ); - const emojis = await fetchAndCheckEtag(locale, path); + const emojis = await fetchAndCheckEtag({ + etagString: locale, + path: localeToEmojiPath(locale), + }); if (!emojis) { return; } - const flattenedEmojis: FlatCompactEmoji[] = flattenEmojiData(emojis); + + const shortcodesData: ShortcodesDataset[] = []; + if (shortcodes) { + const shortcodesResponse = await fetchAndCheckEtag({ + etagString: `${locale}-shortcodes`, + path: localeToShortcodesPath(locale), + }); + if (shortcodesResponse) { + shortcodesData.push(shortcodesResponse); + } else { + throw new Error(`No shortcodes data found for locale ${locale}`); + } + } + + const flattenedEmojis: FlatCompactEmoji[] = flattenEmojiData( + emojis, + shortcodesData, + ); await putEmojiData(flattenedEmojis, locale); return flattenedEmojis; } export async function importCustomEmojiData() { - const emojis = await fetchAndCheckEtag( - 'custom', - '/api/v1/custom_emojis', - ); + const emojis = await fetchAndCheckEtag({ + etagString: 'custom', + path: '/api/v1/custom_emojis', + }); if (!emojis) { return; } - await putCustomEmojiData(emojis); + await putCustomEmojiData({ emojis, clear: true }); return emojis; } -const modules = import.meta.glob( - '../../../../../node_modules/emojibase-data/**/compact.json', - { - query: '?url', - import: 'default', - }, -); +export async function importLegacyShortcodes() { + const globPaths = import.meta.glob( + // We use import.meta.glob to eagerly load the URL, as the regular import() doesn't work inside the Web Worker. + '../../../../../node_modules/emojibase-data/en/shortcodes/iamcal.json', + { eager: true, import: 'default', query: '?url' }, + ); + const path = Object.values(globPaths)[0]; + if (!path) { + throw new Error('IAMCAL shortcodes path not found'); + } + const shortcodesData = await fetchAndCheckEtag({ + checkEtag: true, + etagString: 'shortcodes', + path, + }); + if (!shortcodesData) { + return; + } + await putLegacyShortcodes(shortcodesData); + return Object.keys(shortcodesData); +} -export function localeToPath(locale: Locale) { +function localeToEmojiPath(locale: Locale) { const key = `../../../../../node_modules/emojibase-data/${locale}/compact.json`; - if (!modules[key] || typeof modules[key] !== 'function') { + const emojiModules = import.meta.glob( + '../../../../../node_modules/emojibase-data/**/compact.json', + { + query: '?url', + import: 'default', + eager: true, + }, + ); + const path = emojiModules[key]; + if (!path) { throw new Error(`Unsupported locale: ${locale}`); } - return modules[key](); + return path; } -export async function fetchAndCheckEtag( - localeString: string, - path: string, -): Promise { - const locale = toSupportedLocaleOrCustom(localeString); +function localeToShortcodesPath(locale: Locale) { + const key = `../../../../../node_modules/emojibase-data/${locale}/shortcodes/cldr.json`; + const shortcodesModules = import.meta.glob( + '../../../../../node_modules/emojibase-data/**/shortcodes/cldr.json', + { + query: '?url', + import: 'default', + eager: true, + }, + ); + const path = shortcodesModules[key]; + if (!path) { + throw new Error(`Unsupported locale for shortcodes: ${locale}`); + } + return path; +} + +async function fetchAndCheckEtag({ + etagString, + path, + checkEtag = false, +}: { + etagString: string; + path: string; + checkEtag?: boolean; +}): Promise { + const etagName = toValidEtagName(etagString); // Use location.origin as this script may be loaded from a CDN domain. const url = new URL(path, location.origin); - const oldEtag = await loadLatestEtag(locale); + const oldEtag = checkEtag ? await loadLatestEtag(etagName) : null; const response = await fetch(url, { headers: { 'Content-Type': 'application/json', @@ -80,19 +152,17 @@ export async function fetchAndCheckEtag( } if (!response.ok) { throw new Error( - `Failed to fetch emoji data for ${locale}: ${response.statusText}`, + `Failed to fetch emoji data for ${etagName}: ${response.statusText}`, ); } const data = (await response.json()) as ResultType; - if (!Array.isArray(data)) { - throw new Error(`Unexpected data format for ${locale}: expected an array`); - } // Store the ETag for future requests const etag = response.headers.get('ETag'); - if (etag) { - await putLatestEtag(etag, localeString); + if (etag && checkEtag) { + log(`storing new etag for ${etagName}: ${etag}`); + await putLatestEtag(etag, etagName); } return data; diff --git a/app/javascript/mastodon/features/emoji/locale.ts b/app/javascript/mastodon/features/emoji/locale.ts index 8ff23f5161a1a4..f39b56d47c22a6 100644 --- a/app/javascript/mastodon/features/emoji/locale.ts +++ b/app/javascript/mastodon/features/emoji/locale.ts @@ -1,7 +1,8 @@ import type { Locale } from 'emojibase'; import { SUPPORTED_LOCALES } from 'emojibase'; -import type { LocaleOrCustom } from './types'; +import { EMOJI_DB_NAME_SHORTCODES, EMOJI_TYPE_CUSTOM } from './constants'; +import type { EtagTypes, LocaleOrCustom, LocaleWithShortcodes } from './types'; export function toSupportedLocale(localeBase: string): Locale { const locale = localeBase.toLowerCase(); @@ -12,12 +13,35 @@ export function toSupportedLocale(localeBase: string): Locale { } export function toSupportedLocaleOrCustom(locale: string): LocaleOrCustom { - if (locale.toLowerCase() === 'custom') { - return 'custom'; + if (locale.toLowerCase() === EMOJI_TYPE_CUSTOM) { + return EMOJI_TYPE_CUSTOM; } return toSupportedLocale(locale); } +export function toValidEtagName(input: string): EtagTypes { + const lower = input.toLowerCase(); + if (lower === EMOJI_TYPE_CUSTOM || lower === EMOJI_DB_NAME_SHORTCODES) { + return lower; + } + + if (isLocaleWithShortcodes(lower)) { + return lower; + } + + return toSupportedLocale(lower); +} + function isSupportedLocale(locale: string): locale is Locale { - return SUPPORTED_LOCALES.includes(locale.toLowerCase() as Locale); + return SUPPORTED_LOCALES.includes(locale as Locale); +} + +function isLocaleWithShortcodes(input: string): input is LocaleWithShortcodes { + const [baseLocale, shortcodes] = input.split('-'); + return ( + !!baseLocale && + !!shortcodes && + isSupportedLocale(baseLocale) && + shortcodes === EMOJI_DB_NAME_SHORTCODES + ); } diff --git a/app/javascript/mastodon/features/emoji/normalize.test.ts b/app/javascript/mastodon/features/emoji/normalize.test.ts index b4c766996112c3..8222ab81e588eb 100644 --- a/app/javascript/mastodon/features/emoji/normalize.test.ts +++ b/app/javascript/mastodon/features/emoji/normalize.test.ts @@ -33,6 +33,7 @@ describe('emojiToUnicodeHex', () => { ['⚫', '26AB'], ['🖤', '1F5A4'], ['💀', '1F480'], + ['❤️', '2764'], // Checks for trailing variation selector removal. ['💂‍♂️', '1F482-200D-2642-FE0F'], ] as const)( 'emojiToUnicodeHex converts %s to %s', diff --git a/app/javascript/mastodon/features/emoji/normalize.ts b/app/javascript/mastodon/features/emoji/normalize.ts index 38b01d6905349d..24df808ae1fd6a 100644 --- a/app/javascript/mastodon/features/emoji/normalize.ts +++ b/app/javascript/mastodon/features/emoji/normalize.ts @@ -32,6 +32,12 @@ export function emojiToUnicodeHex(emoji: string): string { codes.push(code); } } + + // Handles how Emojibase removes the variation selector for single code emojis. + // See: https://emojibase.dev/docs/spec/#merged-variation-selectors + if (codes.at(1) === VARIATION_SELECTOR_CODE && codes.length === 2) { + codes.pop(); + } return hexNumbersToString(codes); } diff --git a/app/javascript/mastodon/features/emoji/render.test.ts b/app/javascript/mastodon/features/emoji/render.test.ts index 3c96cbfb553da8..dffebd1f8c576c 100644 --- a/app/javascript/mastodon/features/emoji/render.test.ts +++ b/app/javascript/mastodon/features/emoji/render.test.ts @@ -7,6 +7,7 @@ import { stringToEmojiState, tokenizeText, } from './render'; +import type { EmojiStateCustom, EmojiStateUnicode } from './types'; describe('tokenizeText', () => { test('returns an array of text to be a single token', () => { @@ -82,12 +83,8 @@ describe('stringToEmojiState', () => { }); }); - test('returns custom emoji state for valid custom emoji', () => { - expect(stringToEmojiState(':smile:')).toEqual({ - type: 'custom', - code: 'smile', - data: undefined, - }); + test('returns null for custom emoji without data', () => { + expect(stringToEmojiState(':smile:')).toBeNull(); }); test('returns custom emoji state with data when provided', () => { @@ -107,7 +104,6 @@ describe('stringToEmojiState', () => { test('returns null for invalid emoji strings', () => { expect(stringToEmojiState('notanemoji')).toBeNull(); - expect(stringToEmojiState(':invalid-emoji:')).toBeNull(); }); }); @@ -120,44 +116,71 @@ describe('loadEmojiDataToState', () => { const dbCall = vi .spyOn(db, 'loadEmojiByHexcode') .mockResolvedValue(unicodeEmojiFactory()); - const unicodeState = { type: 'unicode', code: '1F60A' } as const; + const dbLegacyCall = vi + .spyOn(db, 'loadLegacyShortcodesByShortcode') + .mockResolvedValueOnce({ + shortcodes: ['legacy_code'], + hexcode: '1F60A', + }); + const unicodeState = { + type: 'unicode', + code: '1F60A', + } as const satisfies EmojiStateUnicode; const result = await loadEmojiDataToState(unicodeState, 'en'); expect(dbCall).toHaveBeenCalledWith('1F60A', 'en'); + expect(dbLegacyCall).toHaveBeenCalledWith('1F60A'); expect(result).toEqual({ type: 'unicode', code: '1F60A', data: unicodeEmojiFactory(), + shortcode: 'legacy_code', }); }); - test('loads custom emoji data into state', async () => { - const dbCall = vi - .spyOn(db, 'loadCustomEmojiByShortcode') - .mockResolvedValueOnce(customEmojiFactory()); - const customState = { type: 'custom', code: 'smile' } as const; - const result = await loadEmojiDataToState(customState, 'en'); - expect(dbCall).toHaveBeenCalledWith('smile'); - expect(result).toEqual({ + test('returns null for custom emoji without data', async () => { + const customState = { type: 'custom', code: 'smile', - data: customEmojiFactory(), + } as const satisfies EmojiStateCustom; + const result = await loadEmojiDataToState(customState, 'en'); + expect(result).toBeNull(); + }); + + test('loads unicode data using legacy shortcode', async () => { + const dbLegacyCall = vi + .spyOn(db, 'loadLegacyShortcodesByShortcode') + .mockResolvedValueOnce({ + shortcodes: ['test'], + hexcode: 'test', + }); + const dbUnicodeCall = vi + .spyOn(db, 'loadEmojiByHexcode') + .mockResolvedValue(unicodeEmojiFactory()); + const unicodeState = { + type: 'unicode', + code: 'test', + } as const satisfies EmojiStateUnicode; + const result = await loadEmojiDataToState(unicodeState, 'en'); + expect(dbLegacyCall).toHaveBeenCalledWith('test'); + expect(dbUnicodeCall).toHaveBeenCalledWith('test', 'en'); + expect(result).toEqual({ + type: 'unicode', + code: 'test', + data: unicodeEmojiFactory(), + shortcode: 'test', }); }); test('returns null if unicode emoji not found in database', async () => { vi.spyOn(db, 'loadEmojiByHexcode').mockResolvedValueOnce(undefined); - const unicodeState = { type: 'unicode', code: '1F60A' } as const; + const unicodeState = { + type: 'unicode', + code: '1F60A', + } as const satisfies EmojiStateUnicode; const result = await loadEmojiDataToState(unicodeState, 'en'); expect(result).toBeNull(); }); - test('returns null if custom emoji not found in database', async () => { - vi.spyOn(db, 'loadCustomEmojiByShortcode').mockResolvedValueOnce(undefined); - const customState = { type: 'custom', code: 'smile' } as const; - const result = await loadEmojiDataToState(customState, 'en'); - expect(result).toBeNull(); - }); - test('retries loading emoji data once if initial load fails', async () => { const dbCall = vi .spyOn(db, 'loadEmojiByHexcode') @@ -167,7 +190,10 @@ describe('loadEmojiDataToState', () => { .spyOn(console, 'warn') .mockImplementationOnce(() => null); - const unicodeState = { type: 'unicode', code: '1F60A' } as const; + const unicodeState = { + type: 'unicode', + code: '1F60A', + } as const satisfies EmojiStateUnicode; const result = await loadEmojiDataToState(unicodeState, 'en'); expect(dbCall).toHaveBeenCalledTimes(2); diff --git a/app/javascript/mastodon/features/emoji/render.ts b/app/javascript/mastodon/features/emoji/render.ts index 574d5ef59b2484..8fe311014a95c7 100644 --- a/app/javascript/mastodon/features/emoji/render.ts +++ b/app/javascript/mastodon/features/emoji/render.ts @@ -4,12 +4,6 @@ import { EMOJI_TYPE_UNICODE, EMOJI_TYPE_CUSTOM, } from './constants'; -import { - loadCustomEmojiByShortcode, - loadEmojiByHexcode, - LocaleNotLoadedError, -} from './database'; -import { importEmojiData } from './loader'; import { emojiToUnicodeHex } from './normalize'; import type { EmojiLoadedState, @@ -79,7 +73,7 @@ export function tokenizeText(text: string): TokenizedText { export function stringToEmojiState( code: string, customEmoji: ExtraCustomEmojiMap = {}, -): EmojiState | null { +): EmojiStateUnicode | Required | null { if (isUnicodeEmoji(code)) { return { type: EMOJI_TYPE_UNICODE, @@ -89,11 +83,13 @@ export function stringToEmojiState( if (isCustomEmoji(code)) { const shortCode = code.slice(1, -1); - return { - type: EMOJI_TYPE_CUSTOM, - code: shortCode, - data: customEmoji[shortCode], - }; + if (customEmoji[shortCode]) { + return { + type: EMOJI_TYPE_CUSTOM, + code: shortCode, + data: customEmoji[shortCode], + }; + } } return null; @@ -114,26 +110,35 @@ export async function loadEmojiDataToState( return state; } + // Don't try to load data for custom emoji. + if (state.type === EMOJI_TYPE_CUSTOM) { + return null; + } + + const { + loadLegacyShortcodesByShortcode, + loadEmojiByHexcode, + LocaleNotLoadedError, + } = await import('./database'); + // First, try to load the data from IndexedDB. try { + const legacyCode = await loadLegacyShortcodesByShortcode(state.code); // This is duplicative, but that's because TS can't distinguish the state type easily. - if (state.type === EMOJI_TYPE_UNICODE) { - const data = await loadEmojiByHexcode(state.code, locale); - if (data) { - return { - ...state, - data, - }; - } - } else { - const data = await loadCustomEmojiByShortcode(state.code); - if (data) { - return { - ...state, - data, - }; - } + const data = await loadEmojiByHexcode( + legacyCode?.hexcode ?? state.code, + locale, + ); + if (data) { + return { + ...state, + type: EMOJI_TYPE_UNICODE, + data, + // TODO: Use CLDR shortcodes when the picker supports them. + shortcode: legacyCode?.shortcodes.at(0), + }; } + // If not found, assume it's not an emoji and return null. log( 'Could not find emoji %s of type %s for locale %s', @@ -150,6 +155,7 @@ export async function loadEmojiDataToState( state.code, locale, ); + const { importEmojiData } = await import('./loader'); await importEmojiData(locale); // Use this from the loader file as it can be awaited. return loadEmojiDataToState(state, locale, true); } diff --git a/app/javascript/mastodon/features/emoji/types.ts b/app/javascript/mastodon/features/emoji/types.ts index e71fde457a5ec6..06e3a678ec9c98 100644 --- a/app/javascript/mastodon/features/emoji/types.ts +++ b/app/javascript/mastodon/features/emoji/types.ts @@ -4,8 +4,10 @@ import type { FlatCompactEmoji, Locale } from 'emojibase'; import type { ApiCustomEmojiJSON } from '@/mastodon/api_types/custom_emoji'; import type { CustomEmoji } from '@/mastodon/models/custom_emoji'; +import type { RequiredExcept } from '@/mastodon/utils/types'; import type { + EMOJI_DB_NAME_SHORTCODES, EMOJI_MODE_NATIVE, EMOJI_MODE_NATIVE_WITH_FLAGS, EMOJI_MODE_TWEMOJI, @@ -19,6 +21,11 @@ export type EmojiMode = | typeof EMOJI_MODE_TWEMOJI; export type LocaleOrCustom = Locale | typeof EMOJI_TYPE_CUSTOM; +export type LocaleWithShortcodes = `${Locale}-shortcodes`; +export type EtagTypes = + | LocaleOrCustom + | typeof EMOJI_DB_NAME_SHORTCODES + | LocaleWithShortcodes; export interface EmojiAppState { locales: Locale[]; @@ -40,6 +47,7 @@ export interface EmojiStateUnicode { type: typeof EMOJI_TYPE_UNICODE; code: string; data?: UnicodeEmojiData; + shortcode?: string; } export interface EmojiStateCustom { type: typeof EMOJI_TYPE_CUSTOM; @@ -49,7 +57,7 @@ export interface EmojiStateCustom { export type EmojiState = EmojiStateUnicode | EmojiStateCustom; export type EmojiLoadedState = - | Required + | RequiredExcept | Required; export type CustomEmojiMapArg = diff --git a/app/javascript/mastodon/features/emoji/worker.ts b/app/javascript/mastodon/features/emoji/worker.ts index 5360484d7788a5..5602577dbe9ace 100644 --- a/app/javascript/mastodon/features/emoji/worker.ts +++ b/app/javascript/mastodon/features/emoji/worker.ts @@ -1,24 +1,30 @@ -import { importCustomEmojiData, importEmojiData } from './loader'; +import { EMOJI_DB_NAME_SHORTCODES, EMOJI_TYPE_CUSTOM } from './constants'; +import { + importCustomEmojiData, + importEmojiData, + importLegacyShortcodes, +} from './loader'; addEventListener('message', handleMessage); self.postMessage('ready'); // After the worker is ready, notify the main thread -function handleMessage(event: MessageEvent<{ locale: string; path?: string }>) { +function handleMessage(event: MessageEvent<{ locale: string }>) { const { - data: { locale, path }, + data: { locale }, } = event; - void loadData(locale, path); + void loadData(locale); } -async function loadData(locale: string, path?: string) { +async function loadData(locale: string) { let importCount: number | undefined; - if (locale === 'custom') { + if (locale === EMOJI_TYPE_CUSTOM) { importCount = (await importCustomEmojiData())?.length; - } else if (path) { - importCount = (await importEmojiData(locale, path))?.length; + } else if (locale === EMOJI_DB_NAME_SHORTCODES) { + importCount = (await importLegacyShortcodes())?.length; } else { - throw new Error('Path is required for loading locale emoji data'); + importCount = (await importEmojiData(locale))?.length; } + if (importCount) { self.postMessage(`loaded ${importCount} emojis into ${locale}`); } diff --git a/app/javascript/mastodon/features/home_timeline/components/critical_update_banner.tsx b/app/javascript/mastodon/features/home_timeline/components/critical_update_banner.tsx index d0dd2b6acda66f..b57231132f9be3 100644 --- a/app/javascript/mastodon/features/home_timeline/components/critical_update_banner.tsx +++ b/app/javascript/mastodon/features/home_timeline/components/critical_update_banner.tsx @@ -1,26 +1,34 @@ +import type { FC } from 'react'; + import { FormattedMessage } from 'react-intl'; -export const CriticalUpdateBanner = () => ( -
-
-

+import { criticalUpdatesPending } from '@/mastodon/initial_state'; + +export const CriticalUpdateBanner: FC = () => { + if (!criticalUpdatesPending) { + return null; + } + return ( +
+
-

-

- {' '} - +

- -

+ id='home.pending_critical_update.body' + defaultMessage='Please update your Mastodon server as soon as possible!' + />{' '} + + + +

+
-
-); + ); +}; diff --git a/app/javascript/mastodon/features/home_timeline/index.jsx b/app/javascript/mastodon/features/home_timeline/index.jsx index 8c5555fd49e01b..893e2c08cabf38 100644 --- a/app/javascript/mastodon/features/home_timeline/index.jsx +++ b/app/javascript/mastodon/features/home_timeline/index.jsx @@ -15,7 +15,6 @@ import { fetchAnnouncements, toggleShowAnnouncements } from 'mastodon/actions/an import { IconWithBadge } from 'mastodon/components/icon_with_badge'; import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator'; import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; -import { criticalUpdatesPending } from 'mastodon/initial_state'; import { withBreakpoint } from 'mastodon/features/ui/hooks/useBreakpoint'; import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; @@ -27,6 +26,7 @@ import StatusListContainer from '../ui/containers/status_list_container'; import { ColumnSettings } from './components/column_settings'; import { CriticalUpdateBanner } from './components/critical_update_banner'; import { Announcements } from './components/announcements'; +import { AnnualReportTimeline } from '../annual_report/timeline'; const messages = defineMessages({ title: { id: 'column.home', defaultMessage: 'Home' }, @@ -127,7 +127,10 @@ class HomeTimeline extends PureComponent { const { intl, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements, matchesBreakpoint } = this.props; const pinned = !!columnId; const { signedIn } = this.props.identity; - const banners = []; + const banners = [ + , + + ]; let announcementsButton; @@ -145,10 +148,6 @@ class HomeTimeline extends PureComponent { ); } - if (criticalUpdatesPending) { - banners.push(); - } - return ( = ({ + +
diff --git a/app/javascript/mastodon/features/notifications_v2/components/embedded_status_content.tsx b/app/javascript/mastodon/features/notifications_v2/components/embedded_status_content.tsx index 9e7f66d112cd72..7312a94a302b1d 100644 --- a/app/javascript/mastodon/features/notifications_v2/components/embedded_status_content.tsx +++ b/app/javascript/mastodon/features/notifications_v2/components/embedded_status_content.tsx @@ -4,6 +4,7 @@ import type { List } from 'immutable'; import { EmojiHTML } from '@/mastodon/components/emoji/html'; import { useElementHandledLink } from '@/mastodon/components/status/handled_link'; +import type { CustomEmoji } from '@/mastodon/models/custom_emoji'; import type { Status } from '@/mastodon/models/status'; import type { Mention } from './embedded_status'; @@ -33,6 +34,7 @@ export const EmbeddedStatusContent: React.FC<{ className={className} lang={status.get('language') as string} htmlString={status.get('contentHtml') as string} + extraEmojis={status.get('emojis') as List} /> ); }; diff --git a/app/javascript/mastodon/features/ui/components/annual_report_modal.tsx b/app/javascript/mastodon/features/ui/components/annual_report_modal.tsx deleted file mode 100644 index 3a0ea987c619e0..00000000000000 --- a/app/javascript/mastodon/features/ui/components/annual_report_modal.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { useEffect } from 'react'; - -import { AnnualReport } from 'mastodon/features/annual_report'; - -const AnnualReportModal: React.FC<{ - onChangeBackgroundColor: (color: string) => void; -}> = ({ onChangeBackgroundColor }) => { - useEffect(() => { - onChangeBackgroundColor('var(--indigo-1)'); - }, [onChangeBackgroundColor]); - - return ( -
- -
- ); -}; - -// eslint-disable-next-line import/no-default-export -export default AnnualReportModal; diff --git a/app/javascript/mastodon/features/ui/components/media_modal.tsx b/app/javascript/mastodon/features/ui/components/media_modal.tsx index daa0aebeb3f209..ac762aa18d8530 100644 --- a/app/javascript/mastodon/features/ui/components/media_modal.tsx +++ b/app/javascript/mastodon/features/ui/components/media_modal.tsx @@ -114,6 +114,11 @@ export const MediaModal: FC = forwardRef< const bind = useDrag( ({ active, movement: [mx], direction: [xDir], cancel }) => { + // Disable swipe when zoomed in. + if (zoomedIn) { + return; + } + // If dragging and swipe distance is enough, change the index. if ( active && diff --git a/app/javascript/mastodon/features/ui/containers/status_list_container.js b/app/javascript/mastodon/features/ui/containers/status_list_container.js index 66e3b91c7b6d73..1e21730a00798a 100644 --- a/app/javascript/mastodon/features/ui/containers/status_list_container.js +++ b/app/javascript/mastodon/features/ui/containers/status_list_container.js @@ -4,10 +4,10 @@ import { connect } from 'react-redux'; import { debounce } from 'lodash'; -import { scrollTopTimeline, loadPending, TIMELINE_SUGGESTIONS } from '@/mastodon/actions/timelines'; +import { scrollTopTimeline, loadPending } from '@/mastodon/actions/timelines'; +import { isNonStatusId } from '@/mastodon/actions/timelines_typed'; import StatusList from '@/mastodon/components/status_list'; import { me } from '@/mastodon/initial_state'; -import { TIMELINE_WRAPSTODON } from '@/mastodon/reducers/slices/annual_report'; const makeGetStatusIds = (pending = false) => createSelector([ (state, { type }) => state.getIn(['settings', type], ImmutableMap()), @@ -15,7 +15,7 @@ const makeGetStatusIds = (pending = false) => createSelector([ (state) => state.get('statuses'), ], (columnSettings, statusIds, statuses) => { return statusIds.filter(id => { - if (id === null || id === TIMELINE_SUGGESTIONS || id === TIMELINE_WRAPSTODON) return true; + if (isNonStatusId(id)) return true; const statusForId = statuses.get(id); diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index 4cae17255a01ab..3750c7d61454f0 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -21,6 +21,7 @@ import { PictureInPicture } from 'mastodon/features/picture_in_picture'; import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { layoutFromWindow } from 'mastodon/is_mobile'; import { WithRouterPropTypes } from 'mastodon/utils/react_router'; +import { checkAnnualReport } from '@/mastodon/reducers/slices/annual_report'; import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose'; import { clearHeight } from '../../actions/height_cache'; @@ -439,6 +440,7 @@ class UI extends PureComponent { this.props.dispatch(expandHomeTimeline()); this.props.dispatch(fetchNotifications()); this.props.dispatch(fetchServerTranslationLanguages()); + this.props.dispatch(checkAnnualReport()); setTimeout(() => this.props.dispatch(fetchServer()), 3000); } diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index 03f2ea0b4668bd..18c52e712fd9c7 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -283,7 +283,7 @@ export function LinkTimeline () { } export function AnnualReportModal () { - return import('../components/annual_report_modal'); + return import('../../annual_report/modal'); } export function ListEdit () { diff --git a/app/javascript/mastodon/features/video/index.tsx b/app/javascript/mastodon/features/video/index.tsx index aa03e3d2e9d7eb..6721b5c96a281f 100644 --- a/app/javascript/mastodon/features/video/index.tsx +++ b/app/javascript/mastodon/features/video/index.tsx @@ -139,8 +139,8 @@ const persistVolume = (volume: number, muted: boolean) => { }; const restoreVolume = (video: HTMLVideoElement) => { - const volume = (playerSettings.get('volume') as number | undefined) ?? 0.5; - const muted = (playerSettings.get('muted') as boolean | undefined) ?? false; + const volume = playerSettings.get('volume') ?? 0.5; + const muted = playerSettings.get('muted') ?? false; video.volume = volume; video.muted = muted; diff --git a/app/javascript/mastodon/hooks/useDismissible.ts b/app/javascript/mastodon/hooks/useDismissible.ts new file mode 100644 index 00000000000000..95f94d9717147b --- /dev/null +++ b/app/javascript/mastodon/hooks/useDismissible.ts @@ -0,0 +1,42 @@ +import { useCallback, useEffect } from 'react'; + +import type { Map as ImmutableMap } from 'immutable'; + +import { changeSetting } from '@/mastodon/actions/settings'; +import { bannerSettings } from '@/mastodon/settings'; +import { useAppSelector, useAppDispatch } from '@/mastodon/store'; + +export function useDismissible(id: string) { + // We use "dismissed_banners" as that was what this was previously called, + // but we can use this to track any dismissible state. + const dismissed = useAppSelector( + (state) => + !!( + state.settings as ImmutableMap< + 'dismissed_banners', + ImmutableMap + > + ).getIn(['dismissed_banners', id], false), + ); + + const wasDismissed = !!bannerSettings.get(id) || dismissed; + + const dispatch = useAppDispatch(); + + const dismiss = useCallback(() => { + bannerSettings.set(id, true); + dispatch(changeSetting(['dismissed_banners', id], true)); + }, [id, dispatch]); + + useEffect(() => { + // Store legacy localStorage setting on server + if (wasDismissed && !dismissed) { + dispatch(changeSetting(['dismissed_banners', id], true)); + } + }, [id, dispatch, wasDismissed, dismissed]); + + return { + wasDismissed, + dismiss, + }; +} diff --git a/app/javascript/mastodon/initial_state.ts b/app/javascript/mastodon/initial_state.ts index 5d652a0b6dae42..7f00c064942daa 100644 --- a/app/javascript/mastodon/initial_state.ts +++ b/app/javascript/mastodon/initial_state.ts @@ -1,3 +1,4 @@ +import type { ApiAnnualReportState } from './api/annual_report'; import type { ApiAccountJSON } from './api_types/accounts'; type HideItemsDefinition = @@ -68,6 +69,7 @@ interface InitialStateMeta { hide_items: HideItemsDefinition[]; registrations_reach_limit: boolean; simple_timeline_menu: boolean; + wrapstodon?: InitialWrapstodonState | null; } interface Role { @@ -78,6 +80,11 @@ interface Role { highlighted: boolean; } +interface InitialWrapstodonState { + year: number; + state: ApiAnnualReportState; +} + export interface InitialState { accounts: Record; languages: InitialStateLanguage[]; @@ -167,6 +174,7 @@ export const simpleTimelineMenu = getMeta('simple_timeline_menu'); export const communityTimelineInsteadOfSearchMenu = getMeta( 'community_timeline_instead_of_search_menu', ); +export const wrapstodon = getMeta('wrapstodon'); const displayNames = // Intl.DisplayNames can be undefined in old browsers diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json index dd29e459b0773f..2ef46af6d70f28 100644 --- a/app/javascript/mastodon/locales/ar.json +++ b/app/javascript/mastodon/locales/ar.json @@ -107,25 +107,11 @@ "alt_text_modal.describe_for_people_with_visual_impairments": "قم بوصفها للأشخاص ذوي الإعاقة البصرية…", "alt_text_modal.done": "تمّ", "announcement.announcement": "إعلان", - "annual_report.summary.archetype.booster": "The cool-hunter", - "annual_report.summary.archetype.lurker": "المتصفح الصامت", - "annual_report.summary.archetype.oracle": "الحكيم", - "annual_report.summary.archetype.pollster": "مستطلع للرأي", - "annual_report.summary.archetype.replier": "الفراشة الاجتماعية", - "annual_report.summary.followers.followers": "المُتابِعُون", - "annual_report.summary.followers.total": "{count} في المجمل", - "annual_report.summary.here_it_is": "فيما يلي ملخصك لسنة {year}:", - "annual_report.summary.highlighted_post.by_favourites": "المنشور ذو أعلى عدد تفضيلات", - "annual_report.summary.highlighted_post.by_reblogs": "أكثر منشور مُعاد نشره", - "annual_report.summary.highlighted_post.by_replies": "المنشور بأعلى عدد تعليقات", - "annual_report.summary.highlighted_post.possessive": "من قبل {name}", "annual_report.summary.most_used_app.most_used_app": "التطبيق الأكثر استخداماً", "annual_report.summary.most_used_hashtag.most_used_hashtag": "الهاشتاق الأكثر استخداماً", - "annual_report.summary.most_used_hashtag.none": "لا شيء", "annual_report.summary.new_posts.new_posts": "المنشورات الجديدة", "annual_report.summary.percentile.text": "هذا يجعلك من بين أكثر مستخدمي {domain} نشاطاً ", "annual_report.summary.percentile.we_wont_tell_bernie": "سيبقى هذا الأمر بيننا.", - "annual_report.summary.thanks": "شكرا لكونك جزءاً من ماستدون!", "attachments_list.unprocessed": "(غير معالَج)", "audio.hide": "إخفاء المقطع الصوتي", "block_modal.remote_users_caveat": "سوف نطلب من الخادم {domain} أن يحترم قرارك، لكن الالتزام غير مضمون لأن بعض الخواديم قد تتعامل مع نصوص الكتل بشكل مختلف. قد تظل المنشورات العامة مرئية للمستخدمين غير المسجلين الدخول.", diff --git a/app/javascript/mastodon/locales/ast.json b/app/javascript/mastodon/locales/ast.json index 50b0122297d525..b14b0a302730e0 100644 --- a/app/javascript/mastodon/locales/ast.json +++ b/app/javascript/mastodon/locales/ast.json @@ -71,11 +71,7 @@ "alt_text_modal.cancel": "Encaboxar", "alt_text_modal.done": "Fecho", "announcement.announcement": "Anunciu", - "annual_report.summary.followers.followers": "siguidores", - "annual_report.summary.here_it_is": "Equí ta'l to resume de {year}:", - "annual_report.summary.highlighted_post.possessive": "de {name}", "annual_report.summary.new_posts.new_posts": "artículos nuevos", - "annual_report.summary.thanks": "Gracies por ser parte de Mastodon!", "attachments_list.unprocessed": "(ensin procesar)", "block_modal.show_less": "Amosar menos", "block_modal.show_more": "Amosar más", diff --git a/app/javascript/mastodon/locales/az.json b/app/javascript/mastodon/locales/az.json index e895e71921973b..59466034e32559 100644 --- a/app/javascript/mastodon/locales/az.json +++ b/app/javascript/mastodon/locales/az.json @@ -107,25 +107,11 @@ "alt_text_modal.describe_for_people_with_visual_impairments": "Görmə məhdudiyyətli insanlar üçün bunu təsvir et…", "alt_text_modal.done": "Oldu", "announcement.announcement": "Elan", - "annual_report.summary.archetype.booster": "Trend ovçusu", - "annual_report.summary.archetype.lurker": "Lurker", - "annual_report.summary.archetype.oracle": "Orakl", - "annual_report.summary.archetype.pollster": "Sorğu ustası", - "annual_report.summary.archetype.replier": "Sosial kəpənək", - "annual_report.summary.followers.followers": "izləyici", - "annual_report.summary.followers.total": "Cəmi {count}", - "annual_report.summary.here_it_is": "{year} icmalınız:", - "annual_report.summary.highlighted_post.by_favourites": "ən çox sevilən postu", - "annual_report.summary.highlighted_post.by_reblogs": "ən çox təkrar paylaşılan göndəriş", - "annual_report.summary.highlighted_post.by_replies": "ən çox cavabı olan paylaşımı", - "annual_report.summary.highlighted_post.possessive": "{name} istifadəçisinin", "annual_report.summary.most_used_app.most_used_app": "ən çox istifadə etdiyi tətbiq", "annual_report.summary.most_used_hashtag.most_used_hashtag": "ən çox istifadə etdiyi heşteq", - "annual_report.summary.most_used_hashtag.none": "Yoxdur", "annual_report.summary.new_posts.new_posts": "yeni paylaşım", "annual_report.summary.percentile.text": "Bu sizi {domain} istifadəçilərinin ilkqoyur.", "annual_report.summary.percentile.we_wont_tell_bernie": "Bunu Berniyə deməyəcəyik.", - "annual_report.summary.thanks": "Mastodonun bir parçası olduğunuz üçün təşəkkür edirik!", "attachments_list.unprocessed": "(emal edilməyib)", "audio.hide": "Audionu gizlət", "block_modal.remote_users_caveat": "{domain} serverindən qərarınıza hörmət etməsini xahiş edəcəyik. Ancaq, bəzi serverlər əngəlləmələri fərqli şəkildə idarə edə bilər deyə, qərarınıza uymağına zəmanət verilmir. Hər kəsə açıq göndərişlər, hələ də sistemə giriş etməmiş istifadəçilərə görünə bilər.", diff --git a/app/javascript/mastodon/locales/be.json b/app/javascript/mastodon/locales/be.json index ed22eda8d4570f..eb209f2d41936f 100644 --- a/app/javascript/mastodon/locales/be.json +++ b/app/javascript/mastodon/locales/be.json @@ -114,29 +114,51 @@ "alt_text_modal.done": "Гатова", "announcement.announcement": "Аб'ява", "annual_report.announcement.action_build": "Старыць мне Вынікадон", + "annual_report.announcement.action_dismiss": "Не, дзякуй", "annual_report.announcement.action_view": "Паглядзець мой Вынікадон", "annual_report.announcement.description": "Даведайцеся больш пра Вашыя ўзаемадзеянні ў Mastodon за апошні год.", "annual_report.announcement.title": "Вынікадон {year} ужо тут", - "annual_report.summary.archetype.booster": "Паляўнічы на трэнды", - "annual_report.summary.archetype.lurker": "Назіральнік", - "annual_report.summary.archetype.oracle": "Аракул", - "annual_report.summary.archetype.pollster": "Апытвальнік", - "annual_report.summary.archetype.replier": "Душа кампаніі", - "annual_report.summary.followers.followers": "падпісчыкі", - "annual_report.summary.followers.total": "Агулам {count}", - "annual_report.summary.here_it_is": "Вось Вашы вынікі {year} за год:", - "annual_report.summary.highlighted_post.by_favourites": "самы ўпадабаны допіс", - "annual_report.summary.highlighted_post.by_reblogs": "самы пашыраны допіс", - "annual_report.summary.highlighted_post.by_replies": "самы каментаваны допіс", - "annual_report.summary.highlighted_post.possessive": "{name}", + "annual_report.nav_item.badge": "Новы", + "annual_report.shared_page.donate": "Ахвяраваць", + "annual_report.shared_page.footer": "З {heart} згенеравала каманда Mastodon", + "annual_report.shared_page.footer_server_info": "{username} карыстаецца {domain}, адной са шматлікіх супольнасцяў, якія ўтрымлівае Mastodon.", + "annual_report.summary.archetype.booster.desc_public": "{name} увесь час паляваў(-ла) на допісы, каб пашырыць іх і разгаласіць аб іх аўтарах ідэальнымі стрэламі.", + "annual_report.summary.archetype.booster.desc_self": "Вы ўвесь час палявалі на допісы, каб пашырыць іх і разгаласіць аб іх аўтарах ідэальнымі стрэламі.", + "annual_report.summary.archetype.booster.name": "Лучнік", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.desc_public": "Мы ведаем, што {name} быў(-ла) дзесьці тут і па-свойму атрымліваў(-ла) асалоду ад Mastodon.", + "annual_report.summary.archetype.lurker.desc_self": "Мы ведаем, што Вы былі дзесьці тут і па-свойму атрымлівалі асалоду ад Mastodon.", + "annual_report.summary.archetype.lurker.name": "Стоік", + "annual_report.summary.archetype.oracle.desc_public": "Новых допісаў у {name} было больш, чым адказаў, і гэткім чынам ён (яна) пакідаў(-ла) Mastodon свежым і накіраваным у будучыню.", + "annual_report.summary.archetype.oracle.desc_self": "Новых допісаў у Вас было больш, чым адказаў, і гэткім чынам Вы пакідалі Mastodon свежым і накіраваным у будучыню.", + "annual_report.summary.archetype.oracle.name": "Аракул", + "annual_report.summary.archetype.pollster.desc_public": "Сярод допісаў {name} было найбольш апытанак, якімі ён (яна) падымаў(-ла) цікаўнасць у Mastodon.", + "annual_report.summary.archetype.pollster.desc_self": "Сярод Вашых допісаў было найбольш апытанак, якімі Вы падымалі цікаўнасць у Mastodon.", + "annual_report.summary.archetype.pollster.name": "Даследчык", + "annual_report.summary.archetype.replier.desc_public": "{name} часта адказваў(-ла) на допісы іншых людзей, апыляючы Mastodon новымі дыскусіямі.", + "annual_report.summary.archetype.replier.desc_self": "Вы часта адказвалі на допісы іншых людзей, апыляючы Mastodon новымі дыскусіямі.", + "annual_report.summary.archetype.replier.name": "Матыль", + "annual_report.summary.archetype.reveal": "Раскрыць мой архетып", + "annual_report.summary.archetype.reveal_description": "Дзякуй за тое, што Вы былі і застаяцеся часткай Mastodon! Час даведацца, які архетып Вы ўвасобілі ў {year}-ым годзе.", + "annual_report.summary.archetype.title_public": "Архетып {name}", + "annual_report.summary.archetype.title_self": "Ваш архетып", + "annual_report.summary.close": "Закрыць", + "annual_report.summary.copy_link": "Скапіраваць", + "annual_report.summary.followers.new_followers": "{count, plural, one {новы падпісчык} few {новыя падпісчыкі} other {новых падпісчыкаў}}", + "annual_report.summary.highlighted_post.boost_count": "Гэты допіс пашырылі {count, plural, one {адзін раз} few {# разы} other {# разоў}}.", + "annual_report.summary.highlighted_post.favourite_count": "Гэты допіс упадабалі {count, plural, one {адзін раз} few {# разы} other {# разоў}}.", + "annual_report.summary.highlighted_post.reply_count": "На гэты допіс адказалі {count, plural, one {адзін раз} few {# разы} other {# разоў}}.", + "annual_report.summary.highlighted_post.title": "Самы папулярны допіс", "annual_report.summary.most_used_app.most_used_app": "праграма, якой карысталіся найчасцей", "annual_report.summary.most_used_hashtag.most_used_hashtag": "хэштэг, якім карысталіся найчасцей", - "annual_report.summary.most_used_hashtag.none": "Няма", + "annual_report.summary.most_used_hashtag.used_count": "Вы выкарысталі гэты хэштэг у {count, plural, one {адным допісе} other {# допісах}}.", + "annual_report.summary.most_used_hashtag.used_count_public": "{name} выкарыстаў(-ла) гэты хэштэг у {count, plural, one {адным допісе} other {# допісах}}.", "annual_report.summary.new_posts.new_posts": "новыя допісы", "annual_report.summary.percentile.text": "Гэта падымае Вас у топ карыстальнікаў {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "КДБ пра гэта не даведаецца.", + "annual_report.summary.share_elsewhere": "Падзяліцца ў іншым месцы", "annual_report.summary.share_message": "Мой архетып – {archetype}!", - "annual_report.summary.thanks": "Дзякуй за ўдзел у Mastodon!", + "annual_report.summary.share_on_mastodon": "Падзяліцца ў Mastodon", "attachments_list.unprocessed": "(неапрацаваны)", "audio.hide": "Схаваць аўдыя", "block_modal.remote_users_caveat": "Мы папросім сервер {domain} паважаць Ваш выбар. Аднак гэта не гарантуецца, паколькі некаторыя серверы могуць апрацоўваць блакіроўкі іншым чынам. Публічныя паведамленні могуць заставацца бачнымі для ананімных карыстальнікаў.", @@ -419,6 +441,8 @@ "follow_suggestions.who_to_follow": "На каго падпісацца", "followed_tags": "Падпіскі", "footer.about": "Пра нас", + "footer.about_mastodon": "Пра Mastodon", + "footer.about_server": "Пра {domain}", "footer.about_this_server": "Пра сервер", "footer.directory": "Дырэкторыя профіляў", "footer.get_app": "Спампаваць праграму", diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json index 7e8f132d39a266..c6b0f373aa0a7a 100644 --- a/app/javascript/mastodon/locales/bg.json +++ b/app/javascript/mastodon/locales/bg.json @@ -111,25 +111,11 @@ "alt_text_modal.describe_for_people_with_visual_impairments": "Опишете това за хора със зрителни увреждания…", "alt_text_modal.done": "Готово", "announcement.announcement": "Оповестяване", - "annual_report.summary.archetype.booster": "Якият подсилвател", - "annual_report.summary.archetype.lurker": "Дебнещото", - "annual_report.summary.archetype.oracle": "Оракул", - "annual_report.summary.archetype.pollster": "Анкетьорче", - "annual_report.summary.archetype.replier": "Социална пеперуда", - "annual_report.summary.followers.followers": "последователи", - "annual_report.summary.followers.total": "{count} общо", - "annual_report.summary.here_it_is": "Ето преглед на вашата {year} година:", - "annual_report.summary.highlighted_post.by_favourites": "най-правено като любима публикация", - "annual_report.summary.highlighted_post.by_reblogs": "най-подсилваната публикация", - "annual_report.summary.highlighted_post.by_replies": "публикация с най-много отговори", - "annual_report.summary.highlighted_post.possessive": "на {name}", "annual_report.summary.most_used_app.most_used_app": "най-употребявано приложение", "annual_report.summary.most_used_hashtag.most_used_hashtag": "най-употребяван хаштаг", - "annual_report.summary.most_used_hashtag.none": "Няма", "annual_report.summary.new_posts.new_posts": "нови публикации", "annual_report.summary.percentile.text": "Това ви слага най-отгоресред потребителите на {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "Няма да кажем на Бърни Сандърс.", - "annual_report.summary.thanks": "Благодарим, че сте част от Mastodon!", "attachments_list.unprocessed": "(необработено)", "audio.hide": "Скриване на звука", "block_modal.remote_users_caveat": "Ще приканим сървъра {domain} да уважава решението ви. За съжаление не можем да гарантираме това защото някои сървъри могат да третират блокиранията по различен начин. Публичните постове може да продължат да бъдат видими за потребители, които не са се регистрирали.", diff --git a/app/javascript/mastodon/locales/br.json b/app/javascript/mastodon/locales/br.json index d58e8764ed88c4..564630e2d2d08c 100644 --- a/app/javascript/mastodon/locales/br.json +++ b/app/javascript/mastodon/locales/br.json @@ -106,15 +106,8 @@ "alt_text_modal.change_thumbnail": "Kemmañ ar velvenn", "alt_text_modal.done": "Graet", "announcement.announcement": "Kemennad", - "annual_report.summary.followers.followers": "heulier", - "annual_report.summary.followers.total": "{count} en holl", - "annual_report.summary.highlighted_post.by_favourites": "embannadur karet ar muiañ", - "annual_report.summary.highlighted_post.by_reblogs": "embannadur skignet ar muiañ", - "annual_report.summary.highlighted_post.by_replies": "embannadur gant ar muiañ a respontoù", - "annual_report.summary.highlighted_post.possessive": "{name}", "annual_report.summary.most_used_app.most_used_app": "arload muiañ implijet", "annual_report.summary.most_used_hashtag.most_used_hashtag": "ar gerioù-klik implijet ar muiañ", - "annual_report.summary.most_used_hashtag.none": "Hini ebet", "annual_report.summary.new_posts.new_posts": "embannadurioù nevez", "attachments_list.unprocessed": "(ket meret)", "audio.hide": "Kuzhat ar c'hleved", diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index d546426cf22ac8..29ac3879c4c8a3 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -114,29 +114,24 @@ "alt_text_modal.done": "Fet", "announcement.announcement": "Anunci", "annual_report.announcement.action_build": "El meu Wrapstodon s'ha fet", + "annual_report.announcement.action_dismiss": "No, gràcies", "annual_report.announcement.action_view": "Vegeu el meu Wrapstodon", "annual_report.announcement.description": "Descobriu més sobre el vostre involucrament a Mastodon durant l'any passat.", "annual_report.announcement.title": "Ha arribat Wrapstodon {year}", - "annual_report.summary.archetype.booster": "Sempre a la moda", - "annual_report.summary.archetype.lurker": "Tot ho llegeix", - "annual_report.summary.archetype.oracle": "L'Oracle", - "annual_report.summary.archetype.pollster": "Tot són enquestes", - "annual_report.summary.archetype.replier": "Tot ho respon", - "annual_report.summary.followers.followers": "seguidors", - "annual_report.summary.followers.total": "{count} en total", - "annual_report.summary.here_it_is": "El repàs del vostre {year}:", - "annual_report.summary.highlighted_post.by_favourites": "la publicació més afavorida", - "annual_report.summary.highlighted_post.by_reblogs": "la publicació més impulsada", - "annual_report.summary.highlighted_post.by_replies": "la publicació amb més respostes", - "annual_report.summary.highlighted_post.possessive": "de {name}", + "annual_report.nav_item.badge": "Nou", + "annual_report.shared_page.donate": "Fer una donació", + "annual_report.shared_page.footer": "Generat amb {heart} per l'equip de Mastodon", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.copy_link": "Copia l'enllaç", "annual_report.summary.most_used_app.most_used_app": "l'aplicació més utilitzada", "annual_report.summary.most_used_hashtag.most_used_hashtag": "l'etiqueta més utilitzada", - "annual_report.summary.most_used_hashtag.none": "Cap", + "annual_report.summary.most_used_hashtag.used_count_public": "{name} ha inclòs aquesta etiqueta a {count, plural, one {una publicació} other {# publicacions}}.", "annual_report.summary.new_posts.new_posts": "publicacions noves", "annual_report.summary.percentile.text": "Que us posa alcapdamunt dels usuaris de {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "No li ho direm al Bernie.", + "annual_report.summary.share_elsewhere": "Compartir a una altra banda", "annual_report.summary.share_message": "Tinc l'arquetip {archetype}!", - "annual_report.summary.thanks": "Gràcies per formar part de Mastodon!", + "annual_report.summary.share_on_mastodon": "Compartir a Mastodon", "attachments_list.unprocessed": "(sense processar)", "audio.hide": "Amaga l'àudio", "block_modal.remote_users_caveat": "Li demanarem al servidor {domain} que respecti la vostra decisió, tot i que no podem garantir-ho, ja que alguns servidors gestionen de forma diferent els blocatges. És possible que els usuaris no autenticats puguin veure les publicacions públiques.", @@ -362,6 +357,7 @@ "empty_column.notification_requests": "Tot net, ja no hi ha res aquí! Quan rebeu notificacions noves, segons la vostra configuració, apareixeran aquí.", "empty_column.notifications": "Encara no tens notificacions. Quan altre gent interactuï amb tu, les veuràs aquí.", "empty_column.public": "Aquí no hi ha res! Escriu públicament alguna cosa o segueix manualment usuaris d'altres servidors per omplir-ho", + "error.no_hashtag_feed_access": "Creeu un compte o accediu per a veure i seguir aquesta etiqueta.", "error.unexpected_crash.explanation": "A causa d'un error en el nostre codi o d'un problema de compatibilitat amb el navegador, aquesta pàgina no s'ha pogut mostrar correctament.", "error.unexpected_crash.explanation_addons": "Aquesta pàgina no s'ha pogut mostrar correctament. És probable que aquest error sigui causat per un complement del navegador o per eines de traducció automàtica.", "error.unexpected_crash.next_steps": "Prova d'actualitzar la pàgina. Si això no serveix, és possible que encara puguis fer servir Mastodon a través d'un navegador diferent o amb una aplicació nativa.", @@ -908,6 +904,7 @@ "status.edited_x_times": "Editat {count, plural, one {{count} vegada} other {{count} vegades}}", "status.embed": "Obté el codi encastat", "status.favourite": "Favorit", + "status.favourites_count": "{count, plural, one {{counter} favorit} other {{counter} favorits}}", "status.filter": "Filtra aquest tut", "status.history.created": "creat per {name} {date}", "status.history.edited": "editat per {name} {date}", @@ -942,12 +939,14 @@ "status.quotes.empty": "Encara no ha citat aquesta publicació ningú. Quan ho faci algú apareixerà aquí.", "status.quotes.local_other_disclaimer": "No es mostraran les cites rebutjades per l'autor.", "status.quotes.remote_other_disclaimer": "Només es garanteix que es mostraran aquí les cites de {domain}. No es mostraran les rebutjades per l'autor.", + "status.quotes_count": "{count, plural, one {{counter} citació} other {{counter} citacions}}", "status.read_more": "Més informació", "status.reblog": "Impulsa", "status.reblog_or_quote": "Impuls or cita", "status.reblog_private": "Compartiu de nou amb els vostres seguidors", "status.reblogged_by": "impulsat per {name}", "status.reblogs.empty": "Encara no ha impulsat ningú aquest tut. Quan algú ho faci, apareixerà aquí.", + "status.reblogs_count": "{count, plural, one {{counter} impuls} other {{counter} impulsos}}", "status.redraft": "Esborra i reescriu", "status.remove_bookmark": "Elimina el marcador", "status.remove_favourite": "Elimina dels preferits", diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json index b5d594058fec89..3aeb92ec6e5a4e 100644 --- a/app/javascript/mastodon/locales/cs.json +++ b/app/javascript/mastodon/locales/cs.json @@ -114,29 +114,30 @@ "alt_text_modal.done": "Hotovo", "announcement.announcement": "Oznámení", "annual_report.announcement.action_build": "Sestavit můj Wrapstodon", + "annual_report.announcement.action_dismiss": "Ne, děkuji", "annual_report.announcement.action_view": "Zobrazit můj Wrapstodon", "annual_report.announcement.description": "Zjistěte více o vaší aktivitě na Mastodonu za poslední rok.", "annual_report.announcement.title": "Je zde Wrapstodon {year}", - "annual_report.summary.archetype.booster": "Lovec obsahu", - "annual_report.summary.archetype.lurker": "Špión", - "annual_report.summary.archetype.oracle": "Vědma", - "annual_report.summary.archetype.pollster": "Průzkumník", - "annual_report.summary.archetype.replier": "Sociální motýlek", - "annual_report.summary.followers.followers": "sledujících", - "annual_report.summary.followers.total": "{count} celkem", - "annual_report.summary.here_it_is": "Zde je tvůj rok {year} v přehledu:", - "annual_report.summary.highlighted_post.by_favourites": "nejvíce oblíbený příspěvek", - "annual_report.summary.highlighted_post.by_reblogs": "nejvíce boostovaný příspěvek", - "annual_report.summary.highlighted_post.by_replies": "příspěvek s nejvíce odpověďmi", - "annual_report.summary.highlighted_post.possessive": "{name}", + "annual_report.nav_item.badge": "Nový", + "annual_report.shared_page.donate": "Podpořit", + "annual_report.shared_page.footer": "Vytvořeno s {heart} týmem Mastodon", + "annual_report.summary.archetype.reveal": "Odhalit můj archetyp", + "annual_report.summary.archetype.reveal_description": "Děkujeme, že jste součástí Mastodonu! Čas zjistit, který archetype jste ztělesnili v roce {year}.", + "annual_report.summary.archetype.title_public": "Archetyp {name}", + "annual_report.summary.archetype.title_self": "Váš archetyp", + "annual_report.summary.close": "Zavřít", + "annual_report.summary.copy_link": "Zkopírovat odkaz", + "annual_report.summary.highlighted_post.title": "Nejpopulárnější příspěvek", "annual_report.summary.most_used_app.most_used_app": "nejpoužívanější aplikace", "annual_report.summary.most_used_hashtag.most_used_hashtag": "nejpoužívanější hashtag", - "annual_report.summary.most_used_hashtag.none": "Žádné", + "annual_report.summary.most_used_hashtag.used_count": "Tento hashtag jste vložili do {count, plural, one {jednoho příspěvku} few {# příspěvků} many {# příspěvků} other {# příspěvků}}.", + "annual_report.summary.most_used_hashtag.used_count_public": "{name} vložili tento hashtag do {count, plural, one {jednoho příspěveku} few {# příspěvků} many {# příspěvků} other {# příspěvků}}.", "annual_report.summary.new_posts.new_posts": "nové příspěvky", "annual_report.summary.percentile.text": "To vás umisťuje do horních uživatelů domény {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "To, že jste zdejší smetánka, zůstane mezi námi ;).", + "annual_report.summary.share_elsewhere": "Sdílet jinde", "annual_report.summary.share_message": "Mám archetyp {archetype}!", - "annual_report.summary.thanks": "Děkujeme, že jste součástí Mastodonu!", + "annual_report.summary.share_on_mastodon": "Sdílet na Mastodonu", "attachments_list.unprocessed": "(nezpracováno)", "audio.hide": "Skrýt zvuk", "block_modal.remote_users_caveat": "Požádáme server {domain}, aby respektoval vaše rozhodnutí. Úplné dodržování nastavení však není zaručeno, protože některé servery mohou řešit blokování různě. Veřejné příspěvky mohou být stále viditelné pro nepřihlášené uživatele.", diff --git a/app/javascript/mastodon/locales/cy.json b/app/javascript/mastodon/locales/cy.json index fcf33e4635d529..e96a63ff424666 100644 --- a/app/javascript/mastodon/locales/cy.json +++ b/app/javascript/mastodon/locales/cy.json @@ -113,25 +113,50 @@ "alt_text_modal.describe_for_people_with_visual_impairments": "Disgrifiwch hyn ar gyfer pobl â nam ar eu golwg…", "alt_text_modal.done": "Gorffen", "announcement.announcement": "Cyhoeddiad", - "annual_report.summary.archetype.booster": "Y hyrwyddwr", - "annual_report.summary.archetype.lurker": "Y crwydryn", - "annual_report.summary.archetype.oracle": "Yr oracl", - "annual_report.summary.archetype.pollster": "Yr arholwr", - "annual_report.summary.archetype.replier": "Y sbardunwr", - "annual_report.summary.followers.followers": "dilynwyr", - "annual_report.summary.followers.total": "Cyfanswm o{count}", - "annual_report.summary.here_it_is": "Dyma eich {year} yn gryno:", - "annual_report.summary.highlighted_post.by_favourites": "postiad wedi'i ffefrynu fwyaf", - "annual_report.summary.highlighted_post.by_reblogs": "postiad wedi'i hybu fwyaf", - "annual_report.summary.highlighted_post.by_replies": "postiad gyda'r nifer fwyaf o ymatebion", - "annual_report.summary.highlighted_post.possessive": "{name}", + "annual_report.announcement.action_build": "Adeiladu fy Wrapstodon", + "annual_report.announcement.action_dismiss": "Dim diolch", + "annual_report.announcement.action_view": "Gweld fy Wrapstodon", + "annual_report.announcement.description": "Darganfod mwy am eich ymgysylltiad â Mastodon dros y flwyddyn ddiwethaf.", + "annual_report.announcement.title": "Mae Wrapstodon {year} wedi cyrraedd", + "annual_report.nav_item.badge": "Newydd", + "annual_report.shared_page.donate": "Cyfrannu", + "annual_report.shared_page.footer": "Wedi'i gynhyrchu gyda {heart} gan dîm Mastodon", + "annual_report.summary.archetype.booster.desc_public": "Parhaodd {name} i chwilio am bostiadau i'w hybu, gan hyrwyddo crewyr eraill yn effeithiol.", + "annual_report.summary.archetype.booster.desc_self": "Fe wnaethoch chi aros ar y chwilio am bostiadau i'w hybu, gan chwyddo crewyr eraill yn effeithiol.", + "annual_report.summary.archetype.booster.name": "Y Saethydd", + "annual_report.summary.archetype.lurker.desc_public": "Rydyn ni'n gwybod bod {name} allan yna, yn rhywle, yn mwynhau Mastodon yn eu ffordd dawel eu hunain.", + "annual_report.summary.archetype.lurker.desc_self": "Rydyn ni'n gwybod eich bod chi allan yna, yn rhywle, yn mwynhau Mastodon yn eich ffordd dawel eich hun.", + "annual_report.summary.archetype.lurker.name": "Y Stoicaidd", + "annual_report.summary.archetype.oracle.desc_public": "Creodd {name} fwy o bostiadau newydd nag atebion, gan gadw Mastodon yn ffres ac yn edrych i'r dyfodol.", + "annual_report.summary.archetype.oracle.desc_self": "Fe wnaethoch chi greu mwy o bostiadau newydd nag atebion, gan gadw Mastodon yn ffres ac yn edrych i'r dyfodol.", + "annual_report.summary.archetype.oracle.name": "Yr Oracl", + "annual_report.summary.archetype.pollster.desc_public": "Creodd {name} fwy o arolygon barn na mathau eraill o bostiadau, gan feithrin chwilfrydedd am Mastodon.", + "annual_report.summary.archetype.pollster.desc_self": "Fe greoch chi fwy o arolygon barn na mathau eraill o bostiadau, gan feithrin chwilfrydedd ar Mastodon.", + "annual_report.summary.archetype.pollster.name": "Y Syfrdanwr", + "annual_report.summary.archetype.replier.desc_public": "Roedd {name} yn aml yn ymateb i bostiadau pobl eraill, gan beillio Mastodon gyda thrafodaethau newydd.", + "annual_report.summary.archetype.replier.desc_self": "Roeddech chi'n aml yn ymateb i bostiadau pobl eraill, gan hau Mastodon gyda thrafodaethau newydd.", + "annual_report.summary.archetype.replier.name": "Y Pili-pala", + "annual_report.summary.archetype.reveal": "Datgelu fy nhuedd", + "annual_report.summary.archetype.reveal_description": "Diolch am fod yn rhan o Mastodon! Amser darganfod pa duedd oeddech chi'n ei ymgorffori yn {year}.", + "annual_report.summary.archetype.title_public": "Tuedd {name}", + "annual_report.summary.archetype.title_self": "Eich tuedd", + "annual_report.summary.close": "Cau", + "annual_report.summary.copy_link": "Copïo dolen", + "annual_report.summary.followers.new_followers": "{count, plural, one {dilynwr newydd} other {dilynwr newydd}}", + "annual_report.summary.highlighted_post.boost_count": "Cafodd y postiad hwn hwb {count, plural, one {unwaith} other {# gwaith}}.", + "annual_report.summary.highlighted_post.favourite_count": "Cafodd y postiad hwn ei ffefrynnu {count, plural, one {unwaith} other {# gwaith}}.", + "annual_report.summary.highlighted_post.reply_count": "Cafodd y post hwn {count, plural, one {un ateb} other {# ateb}}.", + "annual_report.summary.highlighted_post.title": "Postiad mwyaf poblogaidd", "annual_report.summary.most_used_app.most_used_app": "ap a ddefnyddiwyd fwyaf", "annual_report.summary.most_used_hashtag.most_used_hashtag": "hashnod a ddefnyddiwyd fwyaf", - "annual_report.summary.most_used_hashtag.none": "Dim", + "annual_report.summary.most_used_hashtag.used_count": "Fe wnaethoch chi gynnwys yr hashnod hwn yn {count, plural, one {un postiad} other {# postiad}}.", + "annual_report.summary.most_used_hashtag.used_count_public": "Mae {name} wedi cynnwys yr hashnod hwn yn {count, plural, one {un postiad} other {# postiad}}.", "annual_report.summary.new_posts.new_posts": "postiadau newydd", "annual_report.summary.percentile.text": "Mae hynny'n eich rhoi chi ymysg yuchaf o ddefnyddwyr {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "Fyddwn ni ddim yn dweud wrth Bernie.", - "annual_report.summary.thanks": "Diolch am fod yn rhan o Mastodon!", + "annual_report.summary.share_elsewhere": "Rhannu mewn mannau eraill", + "annual_report.summary.share_message": "Fy arddull i yw {archetype}!", + "annual_report.summary.share_on_mastodon": "Rhannwch ar Mastodon", "attachments_list.unprocessed": "(heb eu prosesu)", "audio.hide": "Cuddio sain", "block_modal.remote_users_caveat": "Byddwn yn gofyn i'r gweinydd {domain} barchu eich penderfyniad. Fodd bynnag, nid yw cydymffurfiad wedi'i warantu gan y gall rhai gweinyddwyr drin rhwystrau mewn ffyrdd gwahanol. Mae'n bosibl y bydd postiadau cyhoeddus yn dal i fod yn weladwy i ddefnyddwyr nad ydynt wedi mewngofnodi.", @@ -516,6 +541,7 @@ "keyboard_shortcuts.toggle_hidden": "Dangos/cuddio testun tu ôl i CW", "keyboard_shortcuts.toggle_sensitivity": "Dangos/cuddio cyfryngau", "keyboard_shortcuts.toot": "Dechrau post newydd", + "keyboard_shortcuts.top": "Symud i frig y rhestr", "keyboard_shortcuts.translate": "i gyfieithu postiad", "keyboard_shortcuts.unfocus": "Dad-ffocysu ardal cyfansoddi testun/chwilio", "keyboard_shortcuts.up": "Symud yn uwch yn y rhestr", diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index eca4a8b6eb8329..f60719d0ed1b5c 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -114,29 +114,51 @@ "alt_text_modal.done": "Færdig", "announcement.announcement": "Bekendtgørelse", "annual_report.announcement.action_build": "Byg min Wrapstodon", + "annual_report.announcement.action_dismiss": "Nej tak", "annual_report.announcement.action_view": "Vis min Wrapstodon", "annual_report.announcement.description": "Få mere at vide om dit engagement på Mastodon i det forgangne år.", "annual_report.announcement.title": "Wrapstodon {year} er her", - "annual_report.summary.archetype.booster": "Fremhæveren", - "annual_report.summary.archetype.lurker": "Lureren", - "annual_report.summary.archetype.oracle": "Oraklet", - "annual_report.summary.archetype.pollster": "Afstemningsmageren", - "annual_report.summary.archetype.replier": "Den sociale sommerfugl", - "annual_report.summary.followers.followers": "følgere", - "annual_report.summary.followers.total": "{count} i alt", - "annual_report.summary.here_it_is": "Her er dit {year} i sammendrag:", - "annual_report.summary.highlighted_post.by_favourites": "mest favoritmarkerede indlæg", - "annual_report.summary.highlighted_post.by_reblogs": "mest fremhævede indlæg", - "annual_report.summary.highlighted_post.by_replies": "indlæg med flest svar", - "annual_report.summary.highlighted_post.possessive": "{name}s", + "annual_report.nav_item.badge": "Ny", + "annual_report.shared_page.donate": "Donér", + "annual_report.shared_page.footer": "Genereret med {heart} af Mastodon-teamet", + "annual_report.shared_page.footer_server_info": "{username} bruger {domain}, et af mange fællesskaber drevet af Mastodon.", + "annual_report.summary.archetype.booster.desc_public": "{name} fortsatte med at jagte indlæg, der kunne fremhæves, og styrkede andre skabere med perfekt sigte.", + "annual_report.summary.archetype.booster.desc_self": "Du fortsatte med at jagte indlæg, der kunne fremhæves, og styrkede andre skabere med perfekt sigte.", + "annual_report.summary.archetype.booster.name": "Bueskytten", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.desc_public": "Vi ved, at {name} var derude et eller andet sted og nød Mastodon på sin egen stille måde.", + "annual_report.summary.archetype.lurker.desc_self": "Vi ved, at du var derude et eller andet sted og nød Mastodon på din egen stille måde.", + "annual_report.summary.archetype.lurker.name": "Den stoiske", + "annual_report.summary.archetype.oracle.desc_public": "{name} oprettede flere nye indlæg end svar og holdt dermed Mastodon frisk og fremtidsorienteret.", + "annual_report.summary.archetype.oracle.desc_self": "Du oprettede flere nye indlæg end svar og holdt dermed Mastodon frisk og fremtidsorienteret.", + "annual_report.summary.archetype.oracle.name": "Oraklet", + "annual_report.summary.archetype.pollster.desc_public": "{name} oprettede flere afstemninger end andre indlægstyper og opdyrkede nysgerrighed på Mastodon.", + "annual_report.summary.archetype.pollster.desc_self": "Du oprettede flere afstemninger end andre indlægstyper og opdyrkede nysgerrighed på Mastodon.", + "annual_report.summary.archetype.pollster.name": "Den undrende", + "annual_report.summary.archetype.replier.desc_public": "{name} svarede ofte på andres indlæg og berigede Mastodon med nye diskussioner.", + "annual_report.summary.archetype.replier.desc_self": "Du svarede ofte på andres indlæg og berigede Mastodon med nye diskussioner.", + "annual_report.summary.archetype.replier.name": "Sommerfuglen", + "annual_report.summary.archetype.reveal": "Afslør min arketype", + "annual_report.summary.archetype.reveal_description": "Tak fordi du er en del af Mastodon! Nu er det tid til at finde ud af, hvilken arketype du var i {year}.", + "annual_report.summary.archetype.title_public": "{name}'s arketype", + "annual_report.summary.archetype.title_self": "Din arketype", + "annual_report.summary.close": "Luk", + "annual_report.summary.copy_link": "Kopiér link", + "annual_report.summary.followers.new_followers": "{count, plural, one {ny følger} other {nye følgere}}", + "annual_report.summary.highlighted_post.boost_count": "Dette indlæg blev fremhævet {count, plural, one {én gang} other {# gange}}.", + "annual_report.summary.highlighted_post.favourite_count": "Dette indlæg blev favoritmarkeret {count, plural, one {én gang} other {# gange}}.", + "annual_report.summary.highlighted_post.reply_count": "Dette indlæg fik {count, plural, one {ét svar} other {# svar}}.", + "annual_report.summary.highlighted_post.title": "Mest populært indlæg", "annual_report.summary.most_used_app.most_used_app": "mest benyttede app", "annual_report.summary.most_used_hashtag.most_used_hashtag": "mest benyttede hashtag", - "annual_report.summary.most_used_hashtag.none": "Intet", + "annual_report.summary.most_used_hashtag.used_count": "Du har inkluderet dette hashtag i {count, plural, one {ét indlæg} other {# indlæg}}.", + "annual_report.summary.most_used_hashtag.used_count_public": "{name} inkluderede dette hashtag i {count, plural, one {ét indlæg} other {# indlæg}}.", "annual_report.summary.new_posts.new_posts": "nye indlæg", "annual_report.summary.percentile.text": "Dermed er du i topaf {domain}-brugere.", "annual_report.summary.percentile.we_wont_tell_bernie": "Vi fortæller det ikke til nogen.", + "annual_report.summary.share_elsewhere": "Del andetsteds", "annual_report.summary.share_message": "Min arketype er {archetype}!", - "annual_report.summary.thanks": "Tak for at være en del af Mastodon!", + "annual_report.summary.share_on_mastodon": "Del på Mastodon", "attachments_list.unprocessed": "(ubehandlet)", "audio.hide": "Skjul lyd", "block_modal.remote_users_caveat": "Serveren {domain} vil blive bedt om at respektere din beslutning. Overholdelse er dog ikke garanteret, da nogle servere kan håndtere blokke forskelligt. Offentlige indlæg kan stadig være synlige for ikke-indloggede brugere.", @@ -413,12 +435,14 @@ "follow_suggestions.hints.similar_to_recently_followed": "Denne profil svarer til de profiler, som senest er blevet fulgt.", "follow_suggestions.personalized_suggestion": "Personligt forslag", "follow_suggestions.popular_suggestion": "Populært forslag", - "follow_suggestions.popular_suggestion_longer": "Populært på {domain}", + "follow_suggestions.popular_suggestion_longer": "Populær på {domain}", "follow_suggestions.similar_to_recently_followed_longer": "Minder om profiler, du har fulgt for nylig", "follow_suggestions.view_all": "Vis alle", "follow_suggestions.who_to_follow": "Hvem, som skal følges", "followed_tags": "Hashtags, som følges", "footer.about": "Om", + "footer.about_mastodon": "Om Mastodon", + "footer.about_server": "Om {domain}", "footer.about_this_server": "Om", "footer.directory": "Profiloversigt", "footer.get_app": "Hent appen", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index d467dcf7c0f373..6db0567c811cce 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -57,7 +57,7 @@ "account.go_to_profile": "Profil aufrufen", "account.hide_reblogs": "Geteilte Beiträge von @{name} ausblenden", "account.in_memoriam": "Zum Andenken.", - "account.joined_short": "Mitglied seit", + "account.joined_short": "Registriert am", "account.languages": "Ausgewählte Sprachen ändern", "account.link_verified_on": "Das Profil mit dieser E-Mail-Adresse wurde bereits am {date} bestätigt", "account.locked_info": "Die Privatsphäre dieses Kontos wurde auf „geschützt“ gesetzt. Die Person bestimmt manuell, wer ihrem Profil folgen darf.", @@ -114,29 +114,51 @@ "alt_text_modal.done": "Fertig", "announcement.announcement": "Ankündigung", "annual_report.announcement.action_build": "Erstelle mein Wrapstodon", + "annual_report.announcement.action_dismiss": "Nein danke", "annual_report.announcement.action_view": "Mein Wrapstodon anschauen", "annual_report.announcement.description": "Erfahre mehr über deine Aktivitäten auf Mastodon im vergangenen Jahr.", "annual_report.announcement.title": "Wrapstodon {year} ist fertiggestellt", - "annual_report.summary.archetype.booster": "Trendjäger*in", - "annual_report.summary.archetype.lurker": "Beobachter*in", - "annual_report.summary.archetype.oracle": "Universaltalent", - "annual_report.summary.archetype.pollster": "Meinungsforscher*in", - "annual_report.summary.archetype.replier": "Gesellige*r", - "annual_report.summary.followers.followers": "Follower", - "annual_report.summary.followers.total": "{count} insgesamt", - "annual_report.summary.here_it_is": "Dein Jahresrückblick für {year}:", - "annual_report.summary.highlighted_post.by_favourites": "am häufigsten favorisierter Beitrag", - "annual_report.summary.highlighted_post.by_reblogs": "am häufigsten geteilter Beitrag", - "annual_report.summary.highlighted_post.by_replies": "Beitrag mit den meisten Antworten", - "annual_report.summary.highlighted_post.possessive": "{name}", + "annual_report.nav_item.badge": "Neu", + "annual_report.shared_page.donate": "Spenden", + "annual_report.shared_page.footer": "Mit {heart} vom Mastodon-Team erstellt", + "annual_report.shared_page.footer_server_info": "{username} verwendet {domain} – eine von vielen Mastodon-Gemeinschaften.", + "annual_report.summary.archetype.booster.desc_public": "{name} hielt Ausschau nach Beiträgen, um sie zu teilen und den Autor*innen mehr Reichweite zu bescheren.", + "annual_report.summary.archetype.booster.desc_self": "Du hieltest Ausschau nach Beiträgen, um sie zu teilen und den Autor*innen mehr Reichweite zu bescheren.", + "annual_report.summary.archetype.booster.name": "Flitzebogen", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.desc_public": "{name} befand sich irgendwo in den Tiefen von Mastodon und genoss das stille Dasein.", + "annual_report.summary.archetype.lurker.desc_self": "Du befandest dich irgendwo in den Tiefen von Mastodon und genossest das stille Dasein.", + "annual_report.summary.archetype.lurker.name": "Unerschütterliche*r", + "annual_report.summary.archetype.oracle.desc_public": "{name} verfasste mehr Beiträge als Antworten, damit Mastodon abwechslungsreich blieb.", + "annual_report.summary.archetype.oracle.desc_self": "Du verfasstest mehr Beiträge als Antworten, damit Mastodon abwechslungsreich blieb.", + "annual_report.summary.archetype.oracle.name": "Orakel", + "annual_report.summary.archetype.pollster.desc_public": "{name} erstellte mehr Umfragen als alles andere und förderte die Neugier auf Mastodon.", + "annual_report.summary.archetype.pollster.desc_self": "Du erstelltest mehr Umfragen als alles andere und fördertest die Neugier auf Mastodon.", + "annual_report.summary.archetype.pollster.name": "Grübler*in", + "annual_report.summary.archetype.replier.desc_public": "{name} antwortete regelmäßig auf die Beiträge anderer und sorgte für blühende Diskussionen auf Mastodon.", + "annual_report.summary.archetype.replier.desc_self": "Du antwortetest regelmäßig auf die Beiträge anderer und sorgtest für blühende Diskussionen auf Mastodon.", + "annual_report.summary.archetype.replier.name": "Schmetterling", + "annual_report.summary.archetype.reveal": "Enthülle mein Wesen", + "annual_report.summary.archetype.reveal_description": "Danke, dass du Teil von Mastodon bist! Zeit herauszufinden, welches Wesen du {year} verkörpert hast.", + "annual_report.summary.archetype.title_public": "Wesen von {name}", + "annual_report.summary.archetype.title_self": "Mein Wesen", + "annual_report.summary.close": "Schließen", + "annual_report.summary.copy_link": "Link kopieren", + "annual_report.summary.followers.new_followers": "{count, plural, one {neuer Follower} other {neue Follower}}", + "annual_report.summary.highlighted_post.boost_count": "Dieser Beitrag wurde {count, plural, one {1 ×} other {# ×}} geteilt.", + "annual_report.summary.highlighted_post.favourite_count": "Dieser Beitrag wurde {count, plural, one {1 ×} other {# ×}} favorisiert.", + "annual_report.summary.highlighted_post.reply_count": "Dieser Beitrag erhielt {count, plural, one {1 Antwort} other {# Antworten}}.", + "annual_report.summary.highlighted_post.title": "Beliebtester Beitrag", "annual_report.summary.most_used_app.most_used_app": "am häufigsten verwendete App", "annual_report.summary.most_used_hashtag.most_used_hashtag": "am häufigsten verwendeter Hashtag", - "annual_report.summary.most_used_hashtag.none": "Keiner", + "annual_report.summary.most_used_hashtag.used_count": "Du verwendetest diesen Hashtag in {count, plural, one {1 Beitrag} other {# Beiträgen}}.", + "annual_report.summary.most_used_hashtag.used_count_public": "{name} verwendete diesen Hashtag in {count, plural, one {1 Beitrag} other {# Beiträgen}}.", "annual_report.summary.new_posts.new_posts": "neue Beiträge", "annual_report.summary.percentile.text": "Damit gehörst du zu den oberstender Nutzer*innen auf {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "Wir werden Bernie nichts verraten.", - "annual_report.summary.share_message": "Ich habe den {archetype} Archetyp!", - "annual_report.summary.thanks": "Danke, dass du Teil von Mastodon bist!", + "annual_report.summary.share_elsewhere": "Woanders teilen", + "annual_report.summary.share_message": "Ich bin vom Wesen {archetype}!", + "annual_report.summary.share_on_mastodon": "Auf Mastodon teilen", "attachments_list.unprocessed": "(ausstehend)", "audio.hide": "Audio ausblenden", "block_modal.remote_users_caveat": "Wir werden den Server {domain} bitten, deine Entscheidung zu respektieren. Allerdings kann nicht garantiert werden, dass sie eingehalten wird, weil einige Server Blockierungen unterschiedlich handhaben können. Öffentliche Beiträge können für nicht angemeldete Nutzer*innen weiterhin sichtbar sein.", @@ -419,6 +441,8 @@ "follow_suggestions.who_to_follow": "Wem folgen?", "followed_tags": "Abonnierte Hashtags", "footer.about": "Über", + "footer.about_mastodon": "Über Mastodon", + "footer.about_server": "Über {domain}", "footer.about_this_server": "Über", "footer.directory": "Profilverzeichnis", "footer.get_app": "App herunterladen", @@ -937,7 +961,7 @@ "status.quote_error.pending_approval_popout.body": "Auf Mastodon kannst du selbst bestimmen, ob du von anderen zitiert werden darfst oder nicht – oder nur nach individueller Genehmigung. Wir warten in diesem Fall noch auf die Genehmigung des ursprünglichen Profils. Bis dahin steht die Veröffentlichung deines Beitrags mit dem zitierten Post noch aus.", "status.quote_error.revoked": "Beitrag durch Autor*in entfernt", "status.quote_followers_only": "Nur Follower können diesen Beitrag zitieren", - "status.quote_manual_review": "Zitierte*r überprüft manuell", + "status.quote_manual_review": "Autor*in wird deine Anfrage manuell überprüfen", "status.quote_noun": "Zitat", "status.quote_policy_change": "Ändern, wer zitieren darf", "status.quote_post_author": "Zitierter Beitrag von @{name}", @@ -955,7 +979,7 @@ "status.reblogs_count": "{count, plural, one {{counter} × geteilt} other {{counter} × geteilt}}", "status.redraft": "Löschen und neu erstellen", "status.remove_bookmark": "Lesezeichen entfernen", - "status.remove_favourite": "Aus Favoriten entfernen", + "status.remove_favourite": "Favorisierung entfernen", "status.remove_quote": "Entfernen", "status.replied_in_thread": "Antwortete im Thread", "status.replied_to": "Antwortete {name}", @@ -972,7 +996,7 @@ "status.title.with_attachments": "{user} veröffentlichte {attachmentCount, plural, one {ein Medium} other {{attachmentCount} Medien}}", "status.translate": "Übersetzen", "status.translated_from_with": "Aus {lang} mittels {provider} übersetzt", - "status.uncached_media_warning": "Vorschau nicht verfügbar", + "status.uncached_media_warning": "Vorschau noch nicht verfügbar", "status.unmute_conversation": "Stummschaltung der Unterhaltung aufheben", "status.unpin": "Vom Profil lösen", "subscribed_languages.lead": "Nach der Änderung werden nur noch Beiträge in den ausgewählten Sprachen in den Timelines deiner Startseite und deiner Listen angezeigt. Wähle keine Sprache aus, um alle Beiträge zu sehen.", @@ -1008,7 +1032,7 @@ "upload_form.drag_and_drop.on_drag_over": "Der Medienanhang {item} wurde bewegt.", "upload_form.drag_and_drop.on_drag_start": "Der Medienanhang {item} wurde aufgenommen.", "upload_form.edit": "Bearbeiten", - "upload_progress.label": "Wird hochgeladen …", + "upload_progress.label": "Upload läuft …", "upload_progress.processing": "Wird verarbeitet …", "username.taken": "Dieser Profilname ist vergeben. Versuche einen anderen", "video.close": "Video schließen", diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json index 48aad8c926d90b..1e07e97b5152a2 100644 --- a/app/javascript/mastodon/locales/el.json +++ b/app/javascript/mastodon/locales/el.json @@ -47,12 +47,12 @@ "account.follow_request_cancel_short": "Ακύρωση", "account.follow_request_short": "Αίτημα", "account.followers": "Ακόλουθοι", - "account.followers.empty": "Κανείς δεν ακολουθεί αυτόν τον χρήστη ακόμα.", + "account.followers.empty": "Κανείς δεν ακολουθεί αυτόν τον χρήστη ακόμη.", "account.followers_counter": "{count, plural, one {{counter} ακόλουθος} other {{counter} ακόλουθοι}}", "account.followers_you_know_counter": "{counter} που ξέρεις", "account.following": "Ακολουθείτε", "account.following_counter": "{count, plural, one {{counter} ακολουθεί} other {{counter} ακολουθούν}}", - "account.follows.empty": "Αυτός ο χρήστης δεν ακολουθεί κανέναν ακόμα.", + "account.follows.empty": "Αυτός ο χρήστης δεν ακολουθεί κανέναν ακόμη.", "account.follows_you": "Σε ακολουθεί", "account.go_to_profile": "Μετάβαση στο προφίλ", "account.hide_reblogs": "Απόκρυψη ενισχύσεων από @{name}", @@ -114,29 +114,51 @@ "alt_text_modal.done": "Ολοκληρώθηκε", "announcement.announcement": "Ανακοίνωση", "annual_report.announcement.action_build": "Φτιάξε το Wrapstodon μου", + "annual_report.announcement.action_dismiss": "Όχι, ευχαριστώ", "annual_report.announcement.action_view": "Προβολή του Wrapstodon μου", "annual_report.announcement.description": "Ανακαλύψτε περισσότερα για την αλληλεπίδραση σας στο Mastodon κατά τη διάρκεια του περασμένου έτους.", "annual_report.announcement.title": "Το Wrapstodon του {year} έφτασε", - "annual_report.summary.archetype.booster": "Ο κυνηγός των φοβερών", - "annual_report.summary.archetype.lurker": "Ο διακριτικός", - "annual_report.summary.archetype.oracle": "Η Πυθία", - "annual_report.summary.archetype.pollster": "Ο δημοσκόπος", - "annual_report.summary.archetype.replier": "Η κοινωνική πεταλούδα", - "annual_report.summary.followers.followers": "ακόλουθοι", - "annual_report.summary.followers.total": "{count} συνολικά", - "annual_report.summary.here_it_is": "Εδώ είναι το {year} σου σε ανασκόπηση:", - "annual_report.summary.highlighted_post.by_favourites": "πιο αγαπημένη ανάρτηση", - "annual_report.summary.highlighted_post.by_reblogs": "πιο ενισχυμένη ανάρτηση", - "annual_report.summary.highlighted_post.by_replies": "ανάρτηση με τις περισσότερες απαντήσεις", - "annual_report.summary.highlighted_post.possessive": "του χρήστη {name}", + "annual_report.nav_item.badge": "Νέο", + "annual_report.shared_page.donate": "Δωρεά", + "annual_report.shared_page.footer": "Παράχθηκε με {heart} από την ομάδα του Mastodon", + "annual_report.shared_page.footer_server_info": "Ο/Η {username} χρησιμοποιεί το {domain}, μία από τις πολλές κοινότητες που βασίζονται στο Mastodon.", + "annual_report.summary.archetype.booster.desc_public": "Ο/Η {name} έμεινε στο κυνήγι για αναρτήσεις για να τις ενισχύσει, ενισχύοντας άλλους δημιουργούς με τέλειο στόχο.", + "annual_report.summary.archetype.booster.desc_self": "Έμεινες στο κυνήγι για αναρτήσεις για να τις ενισχύσεις, ενισχύοντας άλλους δημιουργούς με τέλειο στόχο.", + "annual_report.summary.archetype.booster.name": "Ο Τοξότης", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.desc_public": "Ξέρουμε ότι ο/η {name} ήταν εκεί έξω, κάπου, απολαμβάνοντας το Mastodon με τον δικό του/της ήσυχο τρόπο.", + "annual_report.summary.archetype.lurker.desc_self": "Ξέρουμε ότι ήσουν εκεί έξω, κάπου, απολαμβάνοντας το Mastodon με τον δικό σου ήσυχο τρόπο.", + "annual_report.summary.archetype.lurker.name": "Ο Στωϊκός", + "annual_report.summary.archetype.oracle.desc_public": "O/H {name} δημιούργησε νέες αναρτήσεις περισσότερο από ότι απαντήσεις, κρατώντας το Mastodon φρέσκο και κοιτώντας προς το μέλλον.", + "annual_report.summary.archetype.oracle.desc_self": "Δημιούργησες νέες αναρτήσεις περισσότερο από ότι απαντήσεις, κρατώντας το Mastodon φρέσκο και κοιτώντας προς το μέλλον.", + "annual_report.summary.archetype.oracle.name": "Η Πυθία", + "annual_report.summary.archetype.pollster.desc_public": "Ο/Η {name} δημιούργησε περισσότερες δημοσκοπήσεις από άλλα είδη αναρτήσεων, καλλιεργώντας περιέργεια στο Mastodon.", + "annual_report.summary.archetype.pollster.desc_self": "Δημιούργησες περισσότερες δημοσκοπήσεις από άλλα είδη αναρτήσεων, καλλιεργώντας περιέργεια στο Mastodon.", + "annual_report.summary.archetype.pollster.name": "Ο Θαυμαστής", + "annual_report.summary.archetype.replier.desc_public": "Ο/Η {name} συχνά απαντούσε σε αναρτήσεις άλλων ανθρώπων, επικονίαζοντας το Mastodon με νέες συζητήσεις.", + "annual_report.summary.archetype.replier.desc_self": "Συχνά απαντούσες σε αναρτήσεις άλλων ανθρώπων, επικονίαζοντας το Mastodon με νέες συζητήσεις.", + "annual_report.summary.archetype.replier.name": "Η Πεταλούδα", + "annual_report.summary.archetype.reveal": "Αποκάλυψε το αρχέτυπο μου", + "annual_report.summary.archetype.reveal_description": "Ευχαριστούμε που είσαι μέρος του Mastodon! Ώρα να μάθεις ποιο αρχέτυπο έχεις ενσαρκώσει το {year}.", + "annual_report.summary.archetype.title_public": "Αρχέτυπο του/της {name}", + "annual_report.summary.archetype.title_self": "Το αρχέτυπο σου", + "annual_report.summary.close": "Κλείσιμο", + "annual_report.summary.copy_link": "Αντιγραφή συνδέσμου", + "annual_report.summary.followers.new_followers": "{count, plural, one {νέος ακόλουθος} other {νέοι ακόλουθοι}}", + "annual_report.summary.highlighted_post.boost_count": "Αυτή η ανάρτηση ενισχύθηκε {count, plural, one {μια φορά} other {# φορές}}.", + "annual_report.summary.highlighted_post.favourite_count": "Αυτή η ανάρτηση αγαπήθηκε {count, plural, one {μια φορά} other {# φορές}}.", + "annual_report.summary.highlighted_post.reply_count": "Αυτή η ανάρτηση πήρε {count, plural, one {μια απάντηση} other {# απαντήσεις}}.", + "annual_report.summary.highlighted_post.title": "Πιο δημοφιλής ανάρτηση", "annual_report.summary.most_used_app.most_used_app": "πιο χρησιμοποιημένη εφαρμογή", "annual_report.summary.most_used_hashtag.most_used_hashtag": "πιο χρησιμοποιημένη ετικέτα", - "annual_report.summary.most_used_hashtag.none": "Κανένα", + "annual_report.summary.most_used_hashtag.used_count": "Έχεις συμπεριλάβει αυτήν την ετικέτα σε {count, plural, one {μια ανάρτηση} other {# αναρτήσεις}}.", + "annual_report.summary.most_used_hashtag.used_count_public": "Ο/Η {name} έχει συμπεριλάβει αυτήν την ετικέτα σε {count, plural, one {μια ανάρτηση} other {# αναρτήσεις}}.", "annual_report.summary.new_posts.new_posts": "νέες αναρτήσεις", "annual_report.summary.percentile.text": "Αυτό σε βάζει στο των κορυφαίων χρηστών του {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "Δεν θα το πούμε στον Bernie.", + "annual_report.summary.share_elsewhere": "Κοινοποίηση αλλού", "annual_report.summary.share_message": "Πήρα το αρχέτυπο {archetype}!", - "annual_report.summary.thanks": "Ευχαριστούμε που συμμετέχεις στο Mastodon!", + "annual_report.summary.share_on_mastodon": "Κοινοποίηση στο Mastodon", "attachments_list.unprocessed": "(μη επεξεργασμένο)", "audio.hide": "Απόκρυψη αρχείου ήχου", "block_modal.remote_users_caveat": "Θα ζητήσουμε από τον διακομιστή {domain} να σεβαστεί την απόφασή σου. Ωστόσο, η συμμόρφωση δεν είναι εγγυημένη δεδομένου ότι ορισμένοι διακομιστές ενδέχεται να χειρίζονται τους αποκλεισμούς διαφορετικά. Οι δημόσιες αναρτήσεις ενδέχεται να είναι ορατές σε μη συνδεδεμένους χρήστες.", @@ -337,30 +359,30 @@ "emoji_button.search_results": "Αποτελέσματα αναζήτησης", "emoji_button.symbols": "Σύμβολα", "emoji_button.travel": "Ταξίδια & Τοποθεσίες", - "empty_column.account_featured.me": "Δεν έχεις αναδείξει τίποτα ακόμα. Γνώριζες ότι μπορείς να αναδείξεις τις ετικέτες που χρησιμοποιείς περισσότερο, ακόμη και τους λογαριασμούς των φίλων σου στο προφίλ σου;", - "empty_column.account_featured.other": "Ο λογαριασμός {acct} δεν αναδείξει τίποτα ακόμα. Γνώριζες ότι μπορείς να αναδείξεις τις ετικέτες που χρησιμοποιείς περισσότερο, ακόμη και τους λογαριασμούς των φίλων σου στο προφίλ σου;", - "empty_column.account_featured_other.unknown": "Αυτός ο λογαριασμός δεν έχει αναδείξει τίποτα ακόμα.", + "empty_column.account_featured.me": "Δεν έχεις αναδείξει τίποτα ακόμη. Γνώριζες ότι μπορείς να αναδείξεις τις ετικέτες που χρησιμοποιείς περισσότερο, ακόμη και τους λογαριασμούς των φίλων σου στο προφίλ σου;", + "empty_column.account_featured.other": "Ο/Η {acct} δεν έχει αναδείξει τίποτα ακόμη. Γνώριζες ότι μπορείς να αναδείξεις τις ετικέτες που χρησιμοποιείς περισσότερο, ακόμη και τους λογαριασμούς των φίλων σου στο προφίλ σου;", + "empty_column.account_featured_other.unknown": "Αυτός ο λογαριασμός δεν έχει αναδείξει τίποτα ακόμη.", "empty_column.account_hides_collections": "Αυτός ο χρήστης έχει επιλέξει να μην καταστήσει αυτές τις πληροφορίες διαθέσιμες", "empty_column.account_suspended": "Λογαριασμός σε αναστολή", "empty_column.account_timeline": "Δεν έχει αναρτήσεις εδώ!", "empty_column.account_unavailable": "Μη διαθέσιμο προφίλ", - "empty_column.blocks": "Δεν έχεις αποκλείσει κανέναν χρήστη ακόμα.", - "empty_column.bookmarked_statuses": "Δεν έχεις καμία ανάρτηση με σελιδοδείκτη ακόμα. Μόλις βάλεις κάποιον, θα εμφανιστεί εδώ.", + "empty_column.blocks": "Δεν έχεις αποκλείσει κανέναν χρήστη ακόμη.", + "empty_column.bookmarked_statuses": "Δεν έχεις καμία ανάρτηση με σελιδοδείκτη ακόμη. Μόλις βάλεις κάποιον, θα εμφανιστεί εδώ.", "empty_column.community": "Η τοπική ροή είναι κενή. Γράψε κάτι δημόσια για να αρχίσει να κυλά η μπάλα!", - "empty_column.direct": "Δεν έχεις καμία προσωπική επισήμανση ακόμα. Όταν στείλεις ή λάβεις μία, θα εμφανιστεί εδώ.", + "empty_column.direct": "Δεν έχεις καμία προσωπική επισήμανση ακόμη. Όταν στείλεις ή λάβεις μία, θα εμφανιστεί εδώ.", "empty_column.disabled_feed": "Αυτή η ροή έχει απενεργοποιηθεί από τους διαχειριστές του διακομιστή σας.", - "empty_column.domain_blocks": "Δεν υπάρχουν αποκλεισμένοι τομείς ακόμα.", + "empty_column.domain_blocks": "Δεν υπάρχουν αποκλεισμένοι τομείς ακόμη.", "empty_column.explore_statuses": "Τίποτα δεν βρίσκεται στις τάσεις αυτή τη στιγμή. Έλεγξε αργότερα!", - "empty_column.favourited_statuses": "Δεν έχεις καμία αγαπημένη ανάρτηση ακόμα. Μόλις αγαπήσεις κάποια, θα εμφανιστεί εδώ.", - "empty_column.favourites": "Κανείς δεν έχει αγαπήσει αυτή την ανάρτηση ακόμα. Μόλις το κάνει κάποιος, θα εμφανιστεί εδώ.", - "empty_column.follow_requests": "Δεν έχεις κανένα αίτημα παρακολούθησης ακόμα. Μόλις λάβεις κάποιο, θα εμφανιστεί εδώ.", - "empty_column.followed_tags": "Δεν έχετε ακολουθήσει ακόμα καμία ετικέτα. Όταν το κάνετε, θα εμφανιστούν εδώ.", - "empty_column.hashtag": "Δεν υπάρχει ακόμα κάτι για αυτή την ετικέτα.", + "empty_column.favourited_statuses": "Δεν έχεις καμία αγαπημένη ανάρτηση ακόμη. Μόλις αγαπήσεις κάποια, θα εμφανιστεί εδώ.", + "empty_column.favourites": "Κανείς δεν έχει αγαπήσει αυτή την ανάρτηση ακόμη. Μόλις το κάνει κάποιος, θα εμφανιστεί εδώ.", + "empty_column.follow_requests": "Δεν έχεις κανένα αίτημα παρακολούθησης ακόμη. Μόλις λάβεις κάποιο, θα εμφανιστεί εδώ.", + "empty_column.followed_tags": "Δεν έχεις ακολουθήσει καμία ετικέτα ακόμη. Όταν το κάνεις, θα εμφανιστούν εδώ.", + "empty_column.hashtag": "Δεν υπάρχει τίποτα σε αυτή την ετικέτα ακόμη.", "empty_column.home": "Η τοπική σου ροή είναι κενή! Πήγαινε στο {public} ή κάνε αναζήτηση για να ξεκινήσεις και να γνωρίσεις άλλους χρήστες.", - "empty_column.list": "Δεν υπάρχει τίποτα σε αυτή τη λίστα ακόμα. Όταν τα μέλη της δημοσιεύσουν νέες καταστάσεις, θα εμφανιστούν εδώ.", - "empty_column.mutes": "Δεν έχεις κανένα χρήστη σε σίγαση ακόμα.", + "empty_column.list": "Δεν υπάρχει τίποτα σε αυτή τη λίστα ακόμη. Όταν τα μέλη της δημοσιεύσουν νέες αναρτήσεις, θα εμφανιστούν εδώ.", + "empty_column.mutes": "Δεν έχεις κανένα χρήστη σε σίγαση ακόμη.", "empty_column.notification_requests": "Όλα καθαρά! Δεν υπάρχει τίποτα εδώ. Όταν λαμβάνεις νέες ειδοποιήσεις, αυτές θα εμφανίζονται εδώ σύμφωνα με τις ρυθμίσεις σου.", - "empty_column.notifications": "Δεν έχεις ειδοποιήσεις ακόμα. Όταν άλλα άτομα αλληλεπιδράσουν μαζί σου, θα το δεις εδώ.", + "empty_column.notifications": "Δεν έχεις ειδοποιήσεις ακόμη. Όταν άλλα άτομα αλληλεπιδράσουν μαζί σου, θα το δεις εδώ.", "empty_column.public": "Δεν υπάρχει τίποτα εδώ! Γράψε κάτι δημόσιο ή ακολούθησε χειροκίνητα χρήστες από άλλους διακομιστές για να τη γεμίσεις", "error.no_hashtag_feed_access": "Γίνετε μέλος ή συνδεθείτε για να δείτε και να ακολουθήσετε αυτήν την ετικέτα.", "error.unexpected_crash.explanation": "Είτε λόγω σφάλματος στον κώδικά μας ή λόγω ασυμβατότητας με τον περιηγητή, η σελίδα δε μπόρεσε να εμφανιστεί σωστά.", @@ -419,6 +441,8 @@ "follow_suggestions.who_to_follow": "Ποιον να ακολουθήσεις", "followed_tags": "Ακολουθούμενες ετικέτες", "footer.about": "Σχετικά με", + "footer.about_mastodon": "Σχετικά με το Mastodon", + "footer.about_server": "Σχετικά με το {domain}", "footer.about_this_server": "Σχετικά με", "footer.directory": "Κατάλογος προφίλ", "footer.get_app": "Αποκτήστε την εφαρμογή", @@ -449,7 +473,7 @@ "hashtag.mute": "Σίγαση #{hashtag}", "hashtag.unfeature": "Να μην αναδεικνύεται στο προφίλ", "hashtag.unfollow": "Διακοπή παρακολούθησης ετικέτας", - "hashtags.and_other": "…και {count, plural, other {# ακόμη}}", + "hashtags.and_other": "…και {count, plural, other {# ακόμα}}", "hints.profiles.followers_may_be_missing": "Μπορεί να λείπουν ακόλουθοι για αυτό το προφίλ.", "hints.profiles.follows_may_be_missing": "Άτομα που ακολουθούνται μπορεί να λείπουν απ' αυτό το προφίλ.", "hints.profiles.posts_may_be_missing": "Κάποιες αναρτήσεις από αυτό το προφίλ μπορεί να λείπουν.", @@ -466,7 +490,7 @@ "home.show_announcements": "Εμφάνιση ανακοινώσεων", "ignore_notifications_modal.disclaimer": "Το Mastodon δε μπορεί να ενημερώσει τους χρήστες ότι αγνόησες τις ειδοποιήσεις του. Η αγνόηση ειδοποιήσεων δεν θα εμποδίσει την αποστολή των ίδιων των μηνυμάτων.", "ignore_notifications_modal.filter_instead": "Φίλτρο αντ' αυτού", - "ignore_notifications_modal.filter_to_act_users": "Θα μπορείς ακόμα να αποδεχθείς, να απορρίψεις ή να αναφέρεις χρήστες", + "ignore_notifications_modal.filter_to_act_users": "Θα μπορείς ακόμη να αποδεχθείς, να απορρίψεις ή να αναφέρεις χρήστες", "ignore_notifications_modal.filter_to_avoid_confusion": "Το φιλτράρισμα βοηθά στην αποφυγή πιθανής σύγχυσης", "ignore_notifications_modal.filter_to_review_separately": "Μπορείς να δεις τις φιλτραρισμένες ειδοποιήσεις ξεχωριστά", "ignore_notifications_modal.ignore": "Αγνόηση ειδοποιήσεων", @@ -552,8 +576,8 @@ "lists.list_members_count": "{count, plural, one {# μέλος} other {# μέλη}}", "lists.list_name": "Όνομα λίστας", "lists.new_list_name": "Νέο όνομα λίστας", - "lists.no_lists_yet": "Δεν υπάρχουν λίστες ακόμα.", - "lists.no_members_yet": "Κανένα μέλος ακόμα.", + "lists.no_lists_yet": "Καμία λίστα ακόμη.", + "lists.no_members_yet": "Κανένα μέλος ακόμη.", "lists.no_results_found": "Δεν βρέθηκαν αποτελέσματα.", "lists.remove_member": "Αφαίρεση", "lists.replies_policy.followed": "Οποιοσδήποτε χρήστης που ακολουθείς", @@ -613,15 +637,15 @@ "notification.admin.report_statuses": "Ο χρήστης {name} ανέφερε τον χρήστη {target} για {category}", "notification.admin.report_statuses_other": "Ο χρήστης {name} ανέφερε τον χρήστη {target}", "notification.admin.sign_up": "{name} έχει εγγραφεί", - "notification.admin.sign_up.name_and_others": "{name} και {count, plural, one {# ακόμη} other {# ακόμη}} έχουν εγγραφεί", + "notification.admin.sign_up.name_and_others": "{name} και {count, plural, one {# ακόμα} other {# ακόμα}} έχουν εγγραφεί", "notification.annual_report.message": "Το #Wrapstodon {year} σε περιμένει! Αποκάλυψε τα στιγμιότυπα της χρονιάς και αξέχαστες στιγμές σου στο Mastodon!", "notification.annual_report.view": "Προβολή #Wrapstodon", "notification.favourite": "{name} αγάπησε την ανάρτηση σου", - "notification.favourite.name_and_others_with_link": "{name} και {count, plural, one {# ακόμη} other {# ακόμη}} αγάπησαν την ανάρτησή σου", + "notification.favourite.name_and_others_with_link": "{name} και {count, plural, one {# ακόμα} other {# ακόμα}} αγάπησαν την ανάρτησή σου", "notification.favourite_pm": "Ο χρήστης {name} αγάπησε την ιδιωτική σου επισήμανση", - "notification.favourite_pm.name_and_others_with_link": "Ο χρήστης {name} και {count, plural, one {# ακόμη} other {# ακόμη}} αγάπησαν την ιδωτική σου επισήμανση", + "notification.favourite_pm.name_and_others_with_link": "Ο χρήστης {name} και {count, plural, one {# ακόμα} other {# ακόμα}} αγάπησαν την ιδωτική σου επισήμανση", "notification.follow": "Ο χρήστης {name} σε ακολούθησε", - "notification.follow.name_and_others": "Ο χρήστης {name} και {count, plural, one {# ακόμη} other {# ακόμη}} σε ακολούθησαν", + "notification.follow.name_and_others": "Ο χρήστης {name} και {count, plural, one {# ακόμα} other {# ακόμα}} σε ακολούθησαν", "notification.follow_request": "Ο/H {name} ζήτησε να σε ακολουθήσει", "notification.follow_request.name_and_others": "{name} και {count, plural, one {# άλλος} other {# άλλοι}} ζήτησαν να σε ακολουθήσουν", "notification.label.mention": "Επισήμανση", @@ -644,7 +668,7 @@ "notification.poll": "Μία ψηφοφορία στην οποία συμμετείχες έχει τελειώσει", "notification.quoted_update": "Ο χρήστης {name} επεξεργάστηκε μία ανάρτηση που παρέθεσες", "notification.reblog": "Ο/Η {name} ενίσχυσε την ανάρτηση σου", - "notification.reblog.name_and_others_with_link": "{name} και {count, plural, one {# ακόμη} other {# ακόμη}} ενίσχυσαν την ανάρτησή σου", + "notification.reblog.name_and_others_with_link": "{name} και {count, plural, one {# ακόμα} other {# ακόμα}} ενίσχυσαν την ανάρτησή σου", "notification.relationships_severance_event": "Χάθηκε η σύνδεση με το {name}", "notification.relationships_severance_event.account_suspension": "Ένας διαχειριστής από το {from} ανέστειλε το {target}, πράγμα που σημαίνει ότι δεν μπορείς πλέον να λαμβάνεις ενημερώσεις από αυτούς ή να αλληλεπιδράς μαζί τους.", "notification.relationships_severance_event.domain_block": "Ένας διαχειριστής από {from} έχει μπλοκάρει το {target}, συμπεριλαμβανομένων {followersCount} από τους ακόλουθούς σου και {followingCount, plural, one {# λογαριασμό} other {# λογαριασμοί}} που ακολουθείς.", @@ -812,7 +836,7 @@ "report.forward": "Προώθηση προς {target}", "report.forward_hint": "Ο λογαριασμός είναι από διαφορετικό διακομιστή. Να σταλεί ανώνυμο αντίγραφο της αναφοράς και εκεί;", "report.mute": "Σίγαση", - "report.mute_explanation": "Δεν θα βλέπεις τις αναρτήσεις του. Εκείνοι μπορούν ακόμα να σε ακολουθούν και να βλέπουν τις αναρτήσεις σου χωρίς να γνωρίζουν ότι είναι σε σίγαση.", + "report.mute_explanation": "Δεν θα βλέπεις τις αναρτήσεις τους. Εκείνοι μπορούν ακόμη να σε ακολουθούν και να βλέπουν τις αναρτήσεις σου χωρίς να γνωρίζουν ότι είναι σε σίγαση.", "report.next": "Επόμενο", "report.placeholder": "Επιπλέον σχόλια", "report.reasons.dislike": "Δεν μου αρέσει", @@ -942,7 +966,7 @@ "status.quote_policy_change": "Αλλάξτε ποιός μπορεί να κάνει παράθεση", "status.quote_post_author": "Παρατίθεται μια ανάρτηση από @{name}", "status.quote_private": "Ιδιωτικές αναρτήσεις δεν μπορούν να παρατεθούν", - "status.quotes.empty": "Κανείς δεν έχει παραθέσει αυτή την ανάρτηση ακόμα. Μόλις το κάνει, θα εμφανιστεί εδώ.", + "status.quotes.empty": "Κανείς δεν έχει παραθέσει αυτή την ανάρτηση ακόμη. Μόλις το κάνει, θα εμφανιστεί εδώ.", "status.quotes.local_other_disclaimer": "Οι παραθέσεις που απορρίφθηκαν από τον συντάκτη δε θα εμφανιστούν.", "status.quotes.remote_other_disclaimer": "Μόνο παραθέσεις από το {domain} είναι εγγυημένες ότι θα εμφανιστούν εδώ. Παραθέσεις που απορρίφθηκαν από τον συντάκτη δε θα εμφανιστούν.", "status.quotes_count": "{count, plural, one {{counter} παράθεση} other {{counter} παραθέσεις}}", @@ -951,7 +975,7 @@ "status.reblog_or_quote": "Ενίσχυση ή παράθεση", "status.reblog_private": "Μοιράσου ξανά με τους ακόλουθούς σου", "status.reblogged_by": "{name} προώθησε", - "status.reblogs.empty": "Κανείς δεν ενίσχυσε αυτή την ανάρτηση ακόμα. Μόλις το κάνει κάποιος, θα εμφανιστεί εδώ.", + "status.reblogs.empty": "Κανείς δεν ενίσχυσε αυτή την ανάρτηση ακόμη. Μόλις το κάνει κάποιος, θα εμφανιστεί εδώ.", "status.reblogs_count": "{count, plural, one {{counter} ενίσχυση} other {{counter} ενισχύσεις}}", "status.redraft": "Σβήσε & ξαναγράψε", "status.remove_bookmark": "Αφαίρεση σελιδοδείκτη", diff --git a/app/javascript/mastodon/locales/en-GB.json b/app/javascript/mastodon/locales/en-GB.json index d49b7d36297c19..9b4a79d32cc5e5 100644 --- a/app/javascript/mastodon/locales/en-GB.json +++ b/app/javascript/mastodon/locales/en-GB.json @@ -114,29 +114,51 @@ "alt_text_modal.done": "Done", "announcement.announcement": "Announcement", "annual_report.announcement.action_build": "Build my Wrapstodon", + "annual_report.announcement.action_dismiss": "No thanks", "annual_report.announcement.action_view": "View my Wrapstodon", "annual_report.announcement.description": "Discover more about your engagement on Mastodon over the past year.", "annual_report.announcement.title": "Wrapstodon {year} has arrived", - "annual_report.summary.archetype.booster": "The cool-hunter", - "annual_report.summary.archetype.lurker": "The lurker", - "annual_report.summary.archetype.oracle": "The oracle", - "annual_report.summary.archetype.pollster": "The pollster", - "annual_report.summary.archetype.replier": "The social butterfly", - "annual_report.summary.followers.followers": "followers", - "annual_report.summary.followers.total": "{count} total", - "annual_report.summary.here_it_is": "Here is your {year} in review:", - "annual_report.summary.highlighted_post.by_favourites": "most favourited post", - "annual_report.summary.highlighted_post.by_reblogs": "most boosted post", - "annual_report.summary.highlighted_post.by_replies": "post with the most replies", - "annual_report.summary.highlighted_post.possessive": "{name}'s", + "annual_report.nav_item.badge": "New", + "annual_report.shared_page.donate": "Donate", + "annual_report.shared_page.footer": "Generated with {heart} by the Mastodon team", + "annual_report.shared_page.footer_server_info": "{username} uses {domain}, one of many communities powered by Mastodon.", + "annual_report.summary.archetype.booster.desc_public": "{name} stayed on the hunt for posts to boost, amplifying other creators with perfect aim.", + "annual_report.summary.archetype.booster.desc_self": "You stayed on the hunt for posts to boost, amplifying other creators with perfect aim.", + "annual_report.summary.archetype.booster.name": "The Archer", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.desc_public": "We know {name} was out there, somewhere, enjoying Mastodon in their own quiet way.", + "annual_report.summary.archetype.lurker.desc_self": "We know you were out there, somewhere, enjoying Mastodon in your own quiet way.", + "annual_report.summary.archetype.lurker.name": "The Stoic", + "annual_report.summary.archetype.oracle.desc_public": "{name} created new posts more than replies, keeping Mastodon fresh and future-facing.", + "annual_report.summary.archetype.oracle.desc_self": "You created new posts more than replies, keeping Mastodon fresh and future-facing.", + "annual_report.summary.archetype.oracle.name": "The Oracle", + "annual_report.summary.archetype.pollster.desc_public": "{name} created more polls than other post types, cultivating curiosity on Mastodon.", + "annual_report.summary.archetype.pollster.desc_self": "You created more polls than other post types, cultivating curiosity on Mastodon.", + "annual_report.summary.archetype.pollster.name": "The Wonderer", + "annual_report.summary.archetype.replier.desc_public": "{name} frequently replied to other people’s posts, pollinating Mastodon with new discussions.", + "annual_report.summary.archetype.replier.desc_self": "You frequently replied to other people’s posts, pollinating Mastodon with new discussions.", + "annual_report.summary.archetype.replier.name": "The Butterfly", + "annual_report.summary.archetype.reveal": "Reveal my archetype", + "annual_report.summary.archetype.reveal_description": "Thanks for being part of Mastodon! Time to find out which archetype you embodied in {year}.", + "annual_report.summary.archetype.title_public": "{name}'s archetype", + "annual_report.summary.archetype.title_self": "Your archetype", + "annual_report.summary.close": "Close", + "annual_report.summary.copy_link": "Copy link", + "annual_report.summary.followers.new_followers": "{count, plural, one {new follower} other {new followers}}", + "annual_report.summary.highlighted_post.boost_count": "This post was boosted {count, plural, one {once} other {# times}}.", + "annual_report.summary.highlighted_post.favourite_count": "This post was favourited {count, plural, one {once} other {# times}}.", + "annual_report.summary.highlighted_post.reply_count": "This post got {count, plural, one {one reply} other {# replies}}.", + "annual_report.summary.highlighted_post.title": "Most popular post", "annual_report.summary.most_used_app.most_used_app": "most used app", "annual_report.summary.most_used_hashtag.most_used_hashtag": "most used hashtag", - "annual_report.summary.most_used_hashtag.none": "None", + "annual_report.summary.most_used_hashtag.used_count": "You included this hashtag in {count, plural, one {one post} other {# posts}}.", + "annual_report.summary.most_used_hashtag.used_count_public": "{name} included this hashtag in {count, plural, one {one post} other {# posts}}.", "annual_report.summary.new_posts.new_posts": "new posts", "annual_report.summary.percentile.text": "That puts you in the topof {domain} users.", "annual_report.summary.percentile.we_wont_tell_bernie": "We won't tell Bernie.", + "annual_report.summary.share_elsewhere": "Share elsewhere", "annual_report.summary.share_message": "I got the {archetype} archetype!", - "annual_report.summary.thanks": "Thanks for being part of Mastodon!", + "annual_report.summary.share_on_mastodon": "Share on Mastodon", "attachments_list.unprocessed": "(unprocessed)", "audio.hide": "Hide audio", "block_modal.remote_users_caveat": "We will ask the server {domain} to respect your decision. However, compliance is not guaranteed since some servers may handle blocks differently. Public posts may still be visible to non-logged-in users.", @@ -265,7 +287,7 @@ "confirmations.quiet_post_quote_info.title": "Quoting quiet public posts", "confirmations.redraft.confirm": "Delete & redraft", "confirmations.redraft.message": "Are you sure you want to delete this post and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.", - "confirmations.redraft.title": "Delete & redraft post?", + "confirmations.redraft.title": "Delete and redraft post?", "confirmations.remove_from_followers.confirm": "Remove follower", "confirmations.remove_from_followers.message": "{name} will stop following you. Are you sure you want to proceed?", "confirmations.remove_from_followers.title": "Remove follower?", @@ -306,7 +328,7 @@ "domain_block_modal.you_will_lose_num_followers": "You will lose {followersCount, plural, one {{followersCountDisplay} follower} other {{followersCountDisplay} followers}} and {followingCount, plural, one {{followingCountDisplay} person you follow} other {{followingCountDisplay} people you follow}}.", "domain_block_modal.you_will_lose_relationships": "You will lose all followers and people you follow from this server.", "domain_block_modal.you_wont_see_posts": "You won't see posts or notifications from users on this server.", - "domain_pill.activitypub_lets_connect": "It lets you connect and interact with people not just on Mastodon, but across different social apps too.", + "domain_pill.activitypub_lets_connect": "It lets you connect and interact with people, not just on Mastodon, but across different social apps too.", "domain_pill.activitypub_like_language": "ActivityPub is like the language Mastodon speaks with other social networks.", "domain_pill.server": "Server", "domain_pill.their_handle": "Their handle:", @@ -317,7 +339,7 @@ "domain_pill.who_they_are": "Since handles say who someone is and where they are, you can interact with people across the social web of .", "domain_pill.who_you_are": "Because your handle says who you are and where you are, people can interact with you across the social web of .", "domain_pill.your_handle": "Your handle:", - "domain_pill.your_server": "Your digital home, where all of your posts live. Don’t like this one? Transfer servers at any time and bring your followers, too.", + "domain_pill.your_server": "Your digital home, where all of your posts live. Don’t like this one? Transfer servers at any time and bring your followers too.", "domain_pill.your_username": "Your unique identifier on this server. It’s possible to find users with the same username on different servers.", "dropdown.empty": "Select an option", "embed.instructions": "Embed this post on your website by copying the code below.", @@ -340,7 +362,7 @@ "empty_column.account_featured.me": "You have not featured anything yet. Did you know that you can feature your hashtags you use the most, and even your friend’s accounts on your profile?", "empty_column.account_featured.other": "{acct} has not featured anything yet. Did you know that you can feature your hashtags you use the most, and even your friend’s accounts on your profile?", "empty_column.account_featured_other.unknown": "This account has not featured anything yet.", - "empty_column.account_hides_collections": "This user has chosen to not make this information available", + "empty_column.account_hides_collections": "This user has chosen not to make this information available", "empty_column.account_suspended": "Account suspended", "empty_column.account_timeline": "No posts here!", "empty_column.account_unavailable": "Profile unavailable", @@ -359,7 +381,7 @@ "empty_column.home": "Your home timeline is empty! Follow more people to fill it up.", "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.", "empty_column.mutes": "You haven't muted any users yet.", - "empty_column.notification_requests": "All clear! There is nothing here. When you receive new notifications, they will appear here according to your settings.", + "empty_column.notification_requests": "All clear! There is nothing here. When you receive new notifications, they will appear here, according to your settings.", "empty_column.notifications": "You don't have any notifications yet. When other people interact with you, you will see it here.", "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other servers to fill it up", "error.no_hashtag_feed_access": "Join or log in to view and follow this hashtag.", @@ -419,6 +441,8 @@ "follow_suggestions.who_to_follow": "Who to follow", "followed_tags": "Followed hashtags", "footer.about": "About", + "footer.about_mastodon": "About Mastodon", + "footer.about_server": "About {domain}", "footer.about_this_server": "About", "footer.directory": "Profiles directory", "footer.get_app": "Get the app", @@ -483,7 +507,7 @@ "interaction_modal.on_another_server": "On a different server", "interaction_modal.on_this_server": "On this server", "interaction_modal.title": "Sign in to continue", - "interaction_modal.username_prompt": "E.g. {example}", + "interaction_modal.username_prompt": "Eg {example}", "intervals.full.days": "{number, plural, one {# day} other {# days}}", "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", @@ -601,7 +625,7 @@ "navigation_bar.preferences": "Preferences", "navigation_bar.privacy_and_reach": "Privacy and reach", "navigation_bar.search": "Search", - "navigation_bar.search_trends": "Search / Trending", + "navigation_bar.search_trends": "Search/Trending", "navigation_panel.collapse_followed_tags": "Collapse followed hashtags menu", "navigation_panel.collapse_lists": "Collapse list menu", "navigation_panel.expand_followed_tags": "Expand followed hashtags menu", @@ -641,7 +665,7 @@ "notification.moderation_warning.action_silence": "Your account has been limited.", "notification.moderation_warning.action_suspend": "Your account has been suspended.", "notification.own_poll": "Your poll has ended", - "notification.poll": "A poll you voted in has ended", + "notification.poll": "A poll in which you voted has ended", "notification.quoted_update": "{name} edited a post you have quoted", "notification.reblog": "{name} boosted your post", "notification.reblog.name_and_others_with_link": "{name} and {count, plural, one {# other} other {# others}} boosted your post", @@ -667,7 +691,7 @@ "notification_requests.explainer_for_limited_account": "Notifications from this account have been filtered because the account has been limited by a moderator.", "notification_requests.explainer_for_limited_remote_account": "Notifications from this account have been filtered because the account or its server has been limited by a moderator.", "notification_requests.maximize": "Maximise", - "notification_requests.minimize_banner": "Minimize filtered notifications banner", + "notification_requests.minimize_banner": "Minimise filtered notifications banner", "notification_requests.notifications_from": "Notifications from {name}", "notification_requests.title": "Filtered notifications", "notification_requests.view": "View notifications", @@ -717,11 +741,11 @@ "notifications.policy.filter_limited_accounts_title": "Moderated accounts", "notifications.policy.filter_new_accounts.hint": "Created within the past {days, plural, one {one day} other {# days}}", "notifications.policy.filter_new_accounts_title": "New accounts", - "notifications.policy.filter_not_followers_hint": "Including people who have been following you fewer than {days, plural, one {one day} other {# days}}", + "notifications.policy.filter_not_followers_hint": "Including people who have been following you for fewer than {days, plural, one {one day} other {# days}}", "notifications.policy.filter_not_followers_title": "People not following you", "notifications.policy.filter_not_following_hint": "Until you manually approve them", "notifications.policy.filter_not_following_title": "People you don't follow", - "notifications.policy.filter_private_mentions_hint": "Filtered unless it's in reply to your own mention or if you follow the sender", + "notifications.policy.filter_private_mentions_hint": "Filtered, unless it's in reply to your own mention, or if you follow the sender", "notifications.policy.filter_private_mentions_title": "Unsolicited private mentions", "notifications.policy.title": "Manage notifications from…", "notifications_permission_banner.enable": "Enable desktop notifications", @@ -868,17 +892,17 @@ "search_results.all": "All", "search_results.hashtags": "Hashtags", "search_results.no_results": "No results.", - "search_results.no_search_yet": "Try searching for posts, profiles or hashtags.", + "search_results.no_search_yet": "Try searching for posts, profiles, or hashtags.", "search_results.see_all": "See all", "search_results.statuses": "Posts", "search_results.title": "Search for \"{q}\"", "server_banner.about_active_users": "People using this server during the last 30 days (Monthly Active Users)", "server_banner.active_users": "active users", "server_banner.administered_by": "Administered by:", - "server_banner.is_one_of_many": "{domain} is one of the many independent Mastodon servers you can use to participate in the fediverse.", + "server_banner.is_one_of_many": "{domain} is one of the many independent Mastodon servers you can use to participate in the Fediverse.", "server_banner.server_stats": "Server stats:", "sign_in_banner.create_account": "Create account", - "sign_in_banner.follow_anyone": "Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.", + "sign_in_banner.follow_anyone": "Follow anyone across the Fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.", "sign_in_banner.mastodon_is": "Mastodon is the best way to keep up with what's happening.", "sign_in_banner.sign_in": "Sign in", "sign_in_banner.sso_redirect": "Login or Register", @@ -1020,7 +1044,7 @@ "video.mute": "Mute", "video.pause": "Pause", "video.play": "Play", - "video.skip_backward": "Skip backward", + "video.skip_backward": "Skip backwards", "video.skip_forward": "Skip forward", "video.unmute": "Unmute", "video.volume_down": "Volume down", diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 05ffbb76bc420d..e4348b455f1fbd 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -128,29 +128,51 @@ "alt_text_modal.done": "Done", "announcement.announcement": "Announcement", "annual_report.announcement.action_build": "Build my Wrapstodon", + "annual_report.announcement.action_dismiss": "No thanks", "annual_report.announcement.action_view": "View my Wrapstodon", "annual_report.announcement.description": "Discover more about your engagement on Mastodon over the past year.", "annual_report.announcement.title": "Wrapstodon {year} has arrived", - "annual_report.summary.archetype.booster": "The cool-hunter", - "annual_report.summary.archetype.lurker": "The lurker", - "annual_report.summary.archetype.oracle": "The oracle", - "annual_report.summary.archetype.pollster": "The pollster", - "annual_report.summary.archetype.replier": "The social butterfly", - "annual_report.summary.followers.followers": "followers", - "annual_report.summary.followers.total": "{count} total", - "annual_report.summary.here_it_is": "Here is your {year} in review:", - "annual_report.summary.highlighted_post.by_favourites": "most favourited post", - "annual_report.summary.highlighted_post.by_reblogs": "most boosted post", - "annual_report.summary.highlighted_post.by_replies": "post with the most replies", - "annual_report.summary.highlighted_post.possessive": "{name}'s", + "annual_report.nav_item.badge": "New", + "annual_report.shared_page.donate": "Donate", + "annual_report.shared_page.footer": "Generated with {heart} by the Mastodon team", + "annual_report.shared_page.footer_server_info": "{username} uses {domain}, one of many communities powered by Mastodon.", + "annual_report.summary.archetype.booster.desc_public": "{name} stayed on the hunt for posts to boost, amplifying other creators with perfect aim.", + "annual_report.summary.archetype.booster.desc_self": "You stayed on the hunt for posts to boost, amplifying other creators with perfect aim.", + "annual_report.summary.archetype.booster.name": "The Archer", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.desc_public": "We know {name} was out there, somewhere, enjoying Mastodon in their own quiet way.", + "annual_report.summary.archetype.lurker.desc_self": "We know you were out there, somewhere, enjoying Mastodon in your own quiet way.", + "annual_report.summary.archetype.lurker.name": "The Stoic", + "annual_report.summary.archetype.oracle.desc_public": "{name} created new posts more than replies, keeping Mastodon fresh and future-facing.", + "annual_report.summary.archetype.oracle.desc_self": "You created new posts more than replies, keeping Mastodon fresh and future-facing.", + "annual_report.summary.archetype.oracle.name": "The Oracle", + "annual_report.summary.archetype.pollster.desc_public": "{name} created more polls than other post types, cultivating curiosity on Mastodon.", + "annual_report.summary.archetype.pollster.desc_self": "You created more polls than other post types, cultivating curiosity on Mastodon.", + "annual_report.summary.archetype.pollster.name": "The Wonderer", + "annual_report.summary.archetype.replier.desc_public": "{name} frequently replied to other people’s posts, pollinating Mastodon with new discussions.", + "annual_report.summary.archetype.replier.desc_self": "You frequently replied to other people’s posts, pollinating Mastodon with new discussions.", + "annual_report.summary.archetype.replier.name": "The Butterfly", + "annual_report.summary.archetype.reveal": "Reveal my archetype", + "annual_report.summary.archetype.reveal_description": "Thanks for being part of Mastodon! Time to find out which archetype you embodied in {year}.", + "annual_report.summary.archetype.title_public": "{name}'s archetype", + "annual_report.summary.archetype.title_self": "Your archetype", + "annual_report.summary.close": "Close", + "annual_report.summary.copy_link": "Copy link", + "annual_report.summary.followers.new_followers": "{count, plural, one {new follower} other {new followers}}", + "annual_report.summary.highlighted_post.boost_count": "This post was boosted {count, plural, one {once} other {# times}}.", + "annual_report.summary.highlighted_post.favourite_count": "This post was favorited {count, plural, one {once} other {# times}}.", + "annual_report.summary.highlighted_post.reply_count": "This post got {count, plural, one {one reply} other {# replies}}.", + "annual_report.summary.highlighted_post.title": "Most popular post", "annual_report.summary.most_used_app.most_used_app": "most used app", "annual_report.summary.most_used_hashtag.most_used_hashtag": "most used hashtag", - "annual_report.summary.most_used_hashtag.none": "None", + "annual_report.summary.most_used_hashtag.used_count": "You included this hashtag in {count, plural, one {one post} other {# posts}}.", + "annual_report.summary.most_used_hashtag.used_count_public": "{name} included this hashtag in {count, plural, one {one post} other {# posts}}.", "annual_report.summary.new_posts.new_posts": "new posts", "annual_report.summary.percentile.text": "That puts you in the topof {domain} users.", "annual_report.summary.percentile.we_wont_tell_bernie": "We won't tell Bernie.", + "annual_report.summary.share_elsewhere": "Share elsewhere", "annual_report.summary.share_message": "I got the {archetype} archetype!", - "annual_report.summary.thanks": "Thanks for being part of Mastodon!", + "annual_report.summary.share_on_mastodon": "Share on Mastodon", "antennas.accounts": "{count} accounts", "antennas.add_domain": "Add domain", "antennas.add_domain_placeholder": "New domain", @@ -566,6 +588,8 @@ "follow_suggestions.who_to_follow": "Who to follow", "followed_tags": "Followed hashtags", "footer.about": "About", + "footer.about_mastodon": "About Mastodon", + "footer.about_server": "About {domain}", "footer.about_this_server": "About", "footer.directory": "Profiles directory", "footer.get_app": "Get the app", diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json index f8cfa07f9c787a..f5904154538b50 100644 --- a/app/javascript/mastodon/locales/eo.json +++ b/app/javascript/mastodon/locales/eo.json @@ -28,6 +28,7 @@ "account.disable_notifications": "Ĉesu sciigi min kiam @{name} afiŝas", "account.domain_blocking": "Blokas domajnon", "account.edit_profile": "Redakti la profilon", + "account.edit_profile_short": "Redakti", "account.enable_notifications": "Sciigu min kiam @{name} afiŝos", "account.endorse": "Montri en profilo", "account.familiar_followers_one": "Sekvita de {name1}", @@ -40,6 +41,7 @@ "account.follow": "Sekvi", "account.follow_back": "Sekvu reen", "account.follow_back_short": "Sekvu reen", + "account.follow_request_cancel_short": "Nuligi", "account.followers": "Sekvantoj", "account.followers.empty": "Ankoraŭ neniu sekvas ĉi tiun uzanton.", "account.followers_counter": "{count, plural, one{{counter} sekvanto} other {{counter} sekvantoj}}", @@ -107,25 +109,16 @@ "alt_text_modal.describe_for_people_with_visual_impairments": "Priskribi ĉi tion por homoj kun vidaj malkapabloj…", "alt_text_modal.done": "Farita", "announcement.announcement": "Anonco", - "annual_report.summary.archetype.booster": "La ĉasanto de mojoso", - "annual_report.summary.archetype.lurker": "La vidanto", - "annual_report.summary.archetype.oracle": "La orakolo", - "annual_report.summary.archetype.pollster": "La balotenketisto", - "annual_report.summary.archetype.replier": "La plej societema", - "annual_report.summary.followers.followers": "sekvantoj", - "annual_report.summary.followers.total": "{count} tute", - "annual_report.summary.here_it_is": "Jen via resumo de {year}:", - "annual_report.summary.highlighted_post.by_favourites": "plej ŝatata afiŝo", - "annual_report.summary.highlighted_post.by_reblogs": "plej diskonigita afiŝo", - "annual_report.summary.highlighted_post.by_replies": "afiŝo kun la plej multaj respondoj", - "annual_report.summary.highlighted_post.possessive": "de {name}", + "annual_report.announcement.action_dismiss": "Ne, dankon", + "annual_report.nav_item.badge": "Nova", + "annual_report.shared_page.donate": "Donaci", + "annual_report.summary.copy_link": "Kopii ligilon", + "annual_report.summary.highlighted_post.title": "Plej populara afiŝo", "annual_report.summary.most_used_app.most_used_app": "plej uzita aplikaĵo", "annual_report.summary.most_used_hashtag.most_used_hashtag": "plej uzata kradvorto", - "annual_report.summary.most_used_hashtag.none": "Nenio", "annual_report.summary.new_posts.new_posts": "novaj afiŝoj", "annual_report.summary.percentile.text": "Tion metas vin en la plejde {domain} uzantoj.", "annual_report.summary.percentile.we_wont_tell_bernie": "Ni ne diros al Zamenhof.", - "annual_report.summary.thanks": "Dankon pro esti parto de Mastodon!", "attachments_list.unprocessed": "(neprilaborita)", "audio.hide": "Kaŝi aŭdion", "block_modal.remote_users_caveat": "Ni petos al la servilo {domain} respekti vian elekton. Tamen, plenumo ne estas garantiita ĉar iuj serviloj eble manipulas blokojn malsame. Publikaj afiŝoj eble ankoraŭ estas videbla por ne-ensalutintaj uzantoj.", diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json index b34e4cb3a87270..65c8e7f6411fbb 100644 --- a/app/javascript/mastodon/locales/es-AR.json +++ b/app/javascript/mastodon/locales/es-AR.json @@ -114,29 +114,51 @@ "alt_text_modal.done": "Listo", "announcement.announcement": "Anuncio", "annual_report.announcement.action_build": "Construir mi MastodonAnual", + "annual_report.announcement.action_dismiss": "No, gracias", "annual_report.announcement.action_view": "Ver mi MastodonAnual", "annual_report.announcement.description": "Descubrí más sobre tu participación en Mastodon durante el último año.", "annual_report.announcement.title": "Llegó MastodonAnual {year}", - "annual_report.summary.archetype.booster": "Corrió la voz", - "annual_report.summary.archetype.lurker": "El acechador", - "annual_report.summary.archetype.oracle": "El oráculo", - "annual_report.summary.archetype.pollster": "Estuvo consultando", - "annual_report.summary.archetype.replier": "Respondió un montón", - "annual_report.summary.followers.followers": "seguidores", - "annual_report.summary.followers.total": "{count} en total", - "annual_report.summary.here_it_is": "Acá está tu resumen de {year}:", - "annual_report.summary.highlighted_post.by_favourites": "el mensaje más veces marcado como favorito", - "annual_report.summary.highlighted_post.by_reblogs": "el mensaje que más adhesiones recibió", - "annual_report.summary.highlighted_post.by_replies": "el mensaje que más respuestas recibió", - "annual_report.summary.highlighted_post.possessive": "{name}", + "annual_report.nav_item.badge": "Nueva", + "annual_report.shared_page.donate": "Donar", + "annual_report.shared_page.footer": "Generado con {heart} por el equipo de Mastodon", + "annual_report.shared_page.footer_server_info": "{username} usa {domain}, una de las tantas comunidades de Mastodon.", + "annual_report.summary.archetype.booster.desc_public": "{name} permaneció al acecho en busca de mensajes para adherir, apuntando a otros creadores con perfecta puntería.", + "annual_report.summary.archetype.booster.desc_self": "Permaneciste al acecho en busca de mensajes para adherir, apuntando a otros creadores con perfecta puntería.", + "annual_report.summary.archetype.booster.name": "El arquero", + "annual_report.summary.archetype.die_drei_fragezeichen": "¿¿¿???", + "annual_report.summary.archetype.lurker.desc_public": "Sabemos que {name} estuvo afuera, en algún lado, disfrutando de Mastodon a su propio modo silencioso.", + "annual_report.summary.archetype.lurker.desc_self": "Sabemos que estuviste afuera, en algún lado, disfrutando de Mastodon a tu propio modo silencioso.", + "annual_report.summary.archetype.lurker.name": "El estoico", + "annual_report.summary.archetype.oracle.desc_public": "{name} creó más mensajes nuevos que respuestas, manteniendo a Mastodon fresco y de cara al futuro.", + "annual_report.summary.archetype.oracle.desc_self": "Creaste más mensajes nuevos que respuestas, manteniendo a Mastodon fresco y de cara al futuro.", + "annual_report.summary.archetype.oracle.name": "El oráculo", + "annual_report.summary.archetype.pollster.desc_public": "{name} creó más encuestas que mensajes de otro tipo, cultivando curiosidad en Mastodon.", + "annual_report.summary.archetype.pollster.desc_self": "Creaste más encuestas que mensajes de otro tipo, cultivando curiosidad en Mastodon.", + "annual_report.summary.archetype.pollster.name": "El maravillado", + "annual_report.summary.archetype.replier.desc_public": "{name} respondió frecuentemente a los mensajes de otras cuentas, polinizando con nuevos debates.", + "annual_report.summary.archetype.replier.desc_self": "Respondiste frecuentemente a los mensajes de otras cuentas, polinizando con nuevos debates.", + "annual_report.summary.archetype.replier.name": "La mariposa", + "annual_report.summary.archetype.reveal": "Revelar mi arquetipo", + "annual_report.summary.archetype.reveal_description": "¡Gracias por ser parte de Mastodon! Es hora de descubrir qué arquetipo encarnaste en {year}.", + "annual_report.summary.archetype.title_public": "El arquetipo de {name}", + "annual_report.summary.archetype.title_self": "Tu arquetipo", + "annual_report.summary.close": "Cerrar", + "annual_report.summary.copy_link": "Copiar enlace", + "annual_report.summary.followers.new_followers": "{count, plural, one {nuevo seguidor} other {nuevos seguidores}}", + "annual_report.summary.highlighted_post.boost_count": "Este mensaje recibió adhesiones {count, plural, one {una vez} other {# veces}}.", + "annual_report.summary.highlighted_post.favourite_count": "Este mensaje fue marcado como favorito {count, plural, one {una vez} other {# veces}}.", + "annual_report.summary.highlighted_post.reply_count": "Este mensaje recibió {count, plural, one {una respuesta} other {# respuestas}}.", + "annual_report.summary.highlighted_post.title": "Mensaje más popular", "annual_report.summary.most_used_app.most_used_app": "la aplicación más usada", "annual_report.summary.most_used_hashtag.most_used_hashtag": "la etiqueta más usada", - "annual_report.summary.most_used_hashtag.none": "Ninguna", + "annual_report.summary.most_used_hashtag.used_count": "Incluiste esta etiqueta en {count, plural, one {un mensaje} other {# mensajes}}.", + "annual_report.summary.most_used_hashtag.used_count_public": "{name} incluyó esta etiqueta en {count, plural, one {un mensaje} other {# mensajes}}.", "annual_report.summary.new_posts.new_posts": "nuevos mensajes", "annual_report.summary.percentile.text": "Eso te coloca en la cima delde los usuarios de {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "Queda entre nos.", + "annual_report.summary.share_elsewhere": "Compartir en otro lado", "annual_report.summary.share_message": "¡Conseguí el arquetipo {archetype}!", - "annual_report.summary.thanks": "¡Gracias por ser parte de Mastodon!", + "annual_report.summary.share_on_mastodon": "Compartir en Mastodon", "attachments_list.unprocessed": "[sin procesar]", "audio.hide": "Ocultar audio", "block_modal.remote_users_caveat": "Le pediremos al servidor {domain} que respete tu decisión. Sin embargo, el cumplimiento no está garantizado, ya que algunos servidores pueden manejar los bloqueos de forma diferente. Los mensajes públicos todavía podrían estar visibles para los usuarios no conectados.", @@ -419,6 +441,8 @@ "follow_suggestions.who_to_follow": "A quién seguir", "followed_tags": "Etiquetas seguidas", "footer.about": "Información", + "footer.about_mastodon": "Acerca de Mastodon", + "footer.about_server": "Acerca de {domain}", "footer.about_this_server": "Información", "footer.directory": "Directorio de perfiles", "footer.get_app": "Conseguí la aplicación", diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json index aecb13d8c476c1..3ec8beb6bfa861 100644 --- a/app/javascript/mastodon/locales/es-MX.json +++ b/app/javascript/mastodon/locales/es-MX.json @@ -114,29 +114,51 @@ "alt_text_modal.done": "Hecho", "announcement.announcement": "Anuncio", "annual_report.announcement.action_build": "Preparar mi Wrapstodon", + "annual_report.announcement.action_dismiss": "No, gracias", "annual_report.announcement.action_view": "Ver mi Wrapstodon", "annual_report.announcement.description": "Descubre más sobre tu participación en Mastodon durante el último año.", "annual_report.announcement.title": "Wrapstodon {year} ha llegado", - "annual_report.summary.archetype.booster": "El cazador de tendencias", - "annual_report.summary.archetype.lurker": "El merodeador", - "annual_report.summary.archetype.oracle": "El oráculo", - "annual_report.summary.archetype.pollster": "El encuestador", - "annual_report.summary.archetype.replier": "El más sociable", - "annual_report.summary.followers.followers": "seguidores", - "annual_report.summary.followers.total": "{count} en total", - "annual_report.summary.here_it_is": "Este es el resumen de tu {year}:", - "annual_report.summary.highlighted_post.by_favourites": "publicación con más favoritos", - "annual_report.summary.highlighted_post.by_reblogs": "publicación más impulsada", - "annual_report.summary.highlighted_post.by_replies": "publicación con más respuestas", - "annual_report.summary.highlighted_post.possessive": "de {name}", + "annual_report.nav_item.badge": "Nueva", + "annual_report.shared_page.donate": "Donar", + "annual_report.shared_page.footer": "Generado con {heart} por el equipo de Mastodon", + "annual_report.shared_page.footer_server_info": "{username} usa {domain}, una de las muchas comunidades que funcionan con Mastodon.", + "annual_report.summary.archetype.booster.desc_public": "{name} se mantuvo a la caza de publicaciones para impulsar, dando visibilidad a otros creadores con una puntería perfecta.", + "annual_report.summary.archetype.booster.desc_self": "Te mantuviste a la caza de publicaciones para impulsar, amplificando a otros creadores con una puntería perfecta.", + "annual_report.summary.archetype.booster.name": "El arquero", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.desc_public": "Sabemos que {name} estaba ahí fuera, en algún lugar, disfrutando de Mastodon a su manera tranquila.", + "annual_report.summary.archetype.lurker.desc_self": "Sabemos que estabas ahí fuera, en algún lugar, disfrutando de Mastodon a tu manera, en silencio.", + "annual_report.summary.archetype.lurker.name": "El estoico", + "annual_report.summary.archetype.oracle.desc_public": "{name} creó más publicaciones nuevas que respuestas, lo que mantuvo a Mastodon fresco y con visión de futuro.", + "annual_report.summary.archetype.oracle.desc_self": "Creaste más publicaciones nuevas que respuestas, lo que mantuvo a Mastodon fresco y con visión de futuro.", + "annual_report.summary.archetype.oracle.name": "El oráculo", + "annual_report.summary.archetype.pollster.desc_public": "{name} creó más encuestas que otros tipos de publicaciones, lo que despertó la curiosidad en Mastodon.", + "annual_report.summary.archetype.pollster.desc_self": "Creaste más encuestas que otros tipos de publicaciones, lo que despertó la curiosidad en Mastodon.", + "annual_report.summary.archetype.pollster.name": "El soñador", + "annual_report.summary.archetype.replier.desc_public": "{name} respondía con frecuencia a las publicaciones de otras personas, lo que generaba nuevos debates en Mastodon.", + "annual_report.summary.archetype.replier.desc_self": "Respondías con frecuencia a las publicaciones de otras personas, lo que generaba nuevos debates en Mastodon.", + "annual_report.summary.archetype.replier.name": "La mariposa", + "annual_report.summary.archetype.reveal": "Revelar mi arquetipo", + "annual_report.summary.archetype.reveal_description": "¡Gracias por formar parte de Mastodon! Es hora de descubrir qué arquetipo encarnaste en {year}.", + "annual_report.summary.archetype.title_public": "El arquetipo de {name}", + "annual_report.summary.archetype.title_self": "Tu arquetipo", + "annual_report.summary.close": "Cerrar", + "annual_report.summary.copy_link": "Copiar enlace", + "annual_report.summary.followers.new_followers": "{count, plural,one {nuevo seguidor} other {nuevos seguidores}}", + "annual_report.summary.highlighted_post.boost_count": "Esta publicación fue impulsada {count, plural,one {una vez} other {# veces}}.", + "annual_report.summary.highlighted_post.favourite_count": "Esta publicación fue marcada como favorita {count, plural,one {una vez} other {# veces}}.", + "annual_report.summary.highlighted_post.reply_count": "Esta publicación consiguió {count, plural,one {una respuesta} other {# respuestas}}.", + "annual_report.summary.highlighted_post.title": "Publicación más popular", "annual_report.summary.most_used_app.most_used_app": "aplicación más utilizada", "annual_report.summary.most_used_hashtag.most_used_hashtag": "etiqueta más utilizada", - "annual_report.summary.most_used_hashtag.none": "Ninguna", + "annual_report.summary.most_used_hashtag.used_count": "Incluiste esta etiqueta en {count, plural,one {una publicación} other {# publicaciones}}.", + "annual_report.summary.most_used_hashtag.used_count_public": "{name} incluyó esta etiqueta en {count, plural,one {una publicación} other {# publicaciones}}.", "annual_report.summary.new_posts.new_posts": "nuevas publicaciones", "annual_report.summary.percentile.text": "Eso te sitúa en el topde usuarios de {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "No se lo diremos a Bernie.", + "annual_report.summary.share_elsewhere": "Compartir en otro lado", "annual_report.summary.share_message": "¡Obtuve el arquetipo {archetype}!", - "annual_report.summary.thanks": "¡Gracias por ser parte de Mastodon!", + "annual_report.summary.share_on_mastodon": "Compartir en Mastodon", "attachments_list.unprocessed": "(sin procesar)", "audio.hide": "Ocultar audio", "block_modal.remote_users_caveat": "Le pediremos al servidor {domain} que respete tu decisión. Sin embargo, el cumplimiento no está garantizado, ya que algunos servidores pueden manejar bloques de forma diferente. Las publicaciones públicas pueden ser todavía visibles para los usuarios no conectados.", @@ -419,6 +441,8 @@ "follow_suggestions.who_to_follow": "Recomendamos seguir", "followed_tags": "Etiquetas seguidas", "footer.about": "Acerca de", + "footer.about_mastodon": "Acerca de Mastodon", + "footer.about_server": "Acerca de {domain}", "footer.about_this_server": "Sobre nosotros", "footer.directory": "Directorio de perfiles", "footer.get_app": "Obtener la aplicación", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index 29652f84f4e69c..a09919bdade282 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -114,29 +114,30 @@ "alt_text_modal.done": "Hecho", "announcement.announcement": "Comunicación", "annual_report.announcement.action_build": "Preparar mi Wrapstodon", + "annual_report.announcement.action_dismiss": "No, gracias", "annual_report.announcement.action_view": "Ver mi Wrapstodon", "annual_report.announcement.description": "Descubre más sobre tu participación en Mastodon durante el último año.", "annual_report.announcement.title": "Wrapstodon {year} ha llegado", - "annual_report.summary.archetype.booster": "El cazador de tendencias", - "annual_report.summary.archetype.lurker": "El acechador", - "annual_report.summary.archetype.oracle": "El oráculo", - "annual_report.summary.archetype.pollster": "El encuestador", - "annual_report.summary.archetype.replier": "El más sociable", - "annual_report.summary.followers.followers": "seguidores", - "annual_report.summary.followers.total": "{count} en total", - "annual_report.summary.here_it_is": "Aquí está tu resumen de {year}:", - "annual_report.summary.highlighted_post.by_favourites": "publicación con más favoritos", - "annual_report.summary.highlighted_post.by_reblogs": "publicación más impulsada", - "annual_report.summary.highlighted_post.by_replies": "publicación con más respuestas", - "annual_report.summary.highlighted_post.possessive": "de {name}", + "annual_report.nav_item.badge": "Nuevo", + "annual_report.shared_page.donate": "Donar", + "annual_report.shared_page.footer": "Generado con {heart} por el equipo de Mastodon", + "annual_report.summary.archetype.title_public": "El arquetipo de {name}", + "annual_report.summary.archetype.title_self": "Tu arquetipo", + "annual_report.summary.close": "Cerrar", + "annual_report.summary.copy_link": "Copiar enlace", + "annual_report.summary.highlighted_post.boost_count": "Esta publicación fue impulsada {count, plural,one {una vez} other {# veces}}.", + "annual_report.summary.highlighted_post.reply_count": "Esta publicación recibió {count, plural,one {una respuesta} other {# respuestas}}.", + "annual_report.summary.highlighted_post.title": "Publicación más popular", "annual_report.summary.most_used_app.most_used_app": "aplicación más usada", "annual_report.summary.most_used_hashtag.most_used_hashtag": "etiqueta más usada", - "annual_report.summary.most_used_hashtag.none": "Ninguna", + "annual_report.summary.most_used_hashtag.used_count": "Incluiste esta etiqueta en {count, plural,one {una publicación} other {# publicaciones}}.", + "annual_report.summary.most_used_hashtag.used_count_public": "{name} incluyó esta etiqueta en {count, plural, one {una publicación} other {# publicaciones}}.", "annual_report.summary.new_posts.new_posts": "nuevas publicaciones", "annual_report.summary.percentile.text": "Eso te coloca en el topde usuarios de {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "No se lo diremos a Bernie.", + "annual_report.summary.share_elsewhere": "Compartir en otro lugar", "annual_report.summary.share_message": "¡Obtuve el arquetipo {archetype}!", - "annual_report.summary.thanks": "¡Gracias por ser parte de Mastodon!", + "annual_report.summary.share_on_mastodon": "Compartir en Mastodon", "attachments_list.unprocessed": "(sin procesar)", "audio.hide": "Ocultar audio", "block_modal.remote_users_caveat": "Le pediremos al servidor {domain} que respete tu decisión. Sin embargo, el cumplimiento no está garantizado, ya que algunos servidores pueden manejar bloqueos de forma distinta. Los mensajes públicos pueden ser todavía visibles para los usuarios que no hayan iniciado sesión.", diff --git a/app/javascript/mastodon/locales/et.json b/app/javascript/mastodon/locales/et.json index 7d361cb914abb7..d69b7235d2148c 100644 --- a/app/javascript/mastodon/locales/et.json +++ b/app/javascript/mastodon/locales/et.json @@ -14,7 +14,7 @@ "about.powered_by": "Hajutatud sotsiaalmeedia, mille taga on {mastodon}", "about.rules": "Serveri reeglid", "account.account_note_header": "Isiklik märge", - "account.add_or_remove_from_list": "Lisa või Eemalda loeteludest", + "account.add_or_remove_from_list": "Lisa või eemalda loenditest", "account.badges.bot": "Robot", "account.badges.group": "Grupp", "account.block": "Blokeeri @{name}", @@ -37,7 +37,7 @@ "account.featured": "Esiletõstetud", "account.featured.accounts": "Profiilid", "account.featured.hashtags": "Teemaviited", - "account.featured_tags.last_status_at": "Viimane postitus {date}", + "account.featured_tags.last_status_at": "Viimane postitus: {date}", "account.featured_tags.last_status_never": "Postitusi pole", "account.follow": "Jälgi", "account.follow_back": "Jälgi vastu", @@ -114,29 +114,44 @@ "alt_text_modal.done": "Valmis", "announcement.announcement": "Teadaanne", "annual_report.announcement.action_build": "Koosta kokkuvõte minu tegevusest Mastodonis", + "annual_report.announcement.action_dismiss": "Tänan, ei", "annual_report.announcement.action_view": "Vaata kokkuvõtet minu tegevusest Mastodonis", "annual_report.announcement.description": "Vaata teavet oma suhestumise kohta Mastodonis eelmisel aastal.", "annual_report.announcement.title": "{year}. aasta Mastodoni kokkuvõte on valmis", - "annual_report.summary.archetype.booster": "Ägesisu küttija", - "annual_report.summary.archetype.lurker": "Hiilija", - "annual_report.summary.archetype.oracle": "Oraakel", - "annual_report.summary.archetype.pollster": "Küsitleja", - "annual_report.summary.archetype.replier": "Sotsiaalne liblikas", - "annual_report.summary.followers.followers": "jälgijad", - "annual_report.summary.followers.total": "{count} kokku", - "annual_report.summary.here_it_is": "Siin on sinu {year} ülevaatlikult:", - "annual_report.summary.highlighted_post.by_favourites": "enim lemmikuks märgitud postitus", - "annual_report.summary.highlighted_post.by_reblogs": "enim jagatud postitus", - "annual_report.summary.highlighted_post.by_replies": "kõige vastatum postitus", - "annual_report.summary.highlighted_post.possessive": "omanik {name}", + "annual_report.nav_item.badge": "Uus", + "annual_report.shared_page.donate": "Anneta", + "annual_report.shared_page.footer": "Loodud {heart} Mastodoni meeskonna poolt", + "annual_report.shared_page.footer_server_info": "{username} kasutab {domain}-i, üht paljudest kogukondadest, mis toimivad Mastodonil.", + "annual_report.summary.archetype.booster.desc_public": "{name} jätkas postituste otsimist, et neid edendada, tugevdades teisi loojaid täiusliku täpsusega.", + "annual_report.summary.archetype.booster.desc_self": "Sa jätkasid postituste otsimist, et neid edendada, tugevdades teisi loojaid täiusliku täpsusega.", + "annual_report.summary.archetype.booster.name": "Vibukütt", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.desc_public": "Me teame, et {name} oli kusagil seal väljas ja nautis Mastodoni oma vaiksel moel.", + "annual_report.summary.archetype.lurker.desc_self": "Me teame, et sa olid kusagil seal väljas ja nautisid Mastodoni oma vaiksel moel.", + "annual_report.summary.archetype.lurker.name": "Stoiline", + "annual_report.summary.archetype.oracle.desc_public": "{name} lõi rohkem uusi postitusi kui vastuseid, hoides Mastodoni värskena ja tulevikku suunatuna.", + "annual_report.summary.archetype.oracle.desc_self": "Sa lõid rohkem uusi postitusi kui vastuseid, hoides Mastodoni värskena ja tulevikku suunatuna.", + "annual_report.summary.archetype.oracle.name": "Oraakel", + "annual_report.summary.archetype.pollster.desc_public": "{name} lõi rohkem küsitlusi kui teisi postituse tüüpe, äratades Mastodonis uudishimu.", + "annual_report.summary.archetype.pollster.desc_self": "Sa lõid rohkem küsitlusi kui teisi postituse tüüpe, äratades Mastodonis uudishimu.", + "annual_report.summary.archetype.pollster.name": "Uudishimulik", + "annual_report.summary.archetype.replier.desc_public": "{name} vastas sageli teiste inimeste postitustele, pakkudes Mastodonile uusi aruteluteemasid.", + "annual_report.summary.archetype.replier.desc_self": "Sa vastasid sageli teiste inimeste postitustele, pakkudes Mastodonile uusi aruteluteemasid.", + "annual_report.summary.archetype.replier.name": "Liblikas", + "annual_report.summary.archetype.reveal": "Näita mu põhitüüpi", + "annual_report.summary.archetype.reveal_description": "Täname, et oled Mastodoni liige! Aeg on välja selgitada, millist põhitüüpi sa {year}. kehastasid.", + "annual_report.summary.archetype.title_public": "Kasutaja {name} põhitüüp", + "annual_report.summary.archetype.title_self": "Sinu põhitüüp", + "annual_report.summary.close": "Sule", + "annual_report.summary.copy_link": "Kopeeri link", + "annual_report.summary.highlighted_post.title": "Kõige populaarsemad postitused", "annual_report.summary.most_used_app.most_used_app": "enim kasutatud äpp", "annual_report.summary.most_used_hashtag.most_used_hashtag": "enim kasutatud teemaviide", - "annual_report.summary.most_used_hashtag.none": "Puudub", + "annual_report.summary.most_used_hashtag.used_count": "Sa lisasid selle teemaviite {count, plural, one {ühele postitusele} other {#-le postitusele}}.", "annual_report.summary.new_posts.new_posts": "uus postitus", "annual_report.summary.percentile.text": "See paneb su top {domain} kasutajate hulka.", "annual_report.summary.percentile.we_wont_tell_bernie": "Vägev.", "annual_report.summary.share_message": "Minu arhetüüp on {archetype}!", - "annual_report.summary.thanks": "Tänud olemast osa Mastodonist!", "attachments_list.unprocessed": "(töötlemata)", "audio.hide": "Peida audio", "block_modal.remote_users_caveat": "Serverile {domain} edastatakse palve otsust järgida. Ometi pole see tagatud, kuna mõned serverid võivad blokeeringuid käsitleda omal moel. Avalikud postitused võivad tuvastamata kasutajatele endiselt näha olla.", diff --git a/app/javascript/mastodon/locales/eu.json b/app/javascript/mastodon/locales/eu.json index 688ba0976769ca..8b6699864bf952 100644 --- a/app/javascript/mastodon/locales/eu.json +++ b/app/javascript/mastodon/locales/eu.json @@ -113,25 +113,52 @@ "alt_text_modal.describe_for_people_with_visual_impairments": "Deskribatu hau ikusmen arazoak dituzten pertsonentzat…", "alt_text_modal.done": "Egina", "announcement.announcement": "Iragarpena", - "annual_report.summary.archetype.booster": "Sustatzailea", - "annual_report.summary.archetype.lurker": "Begiluzea", - "annual_report.summary.archetype.oracle": "Orakulua", - "annual_report.summary.archetype.pollster": "Bozketazalea", - "annual_report.summary.archetype.replier": "Tolosa", - "annual_report.summary.followers.followers": "jarraitzaileak", - "annual_report.summary.followers.total": "{count} guztira", - "annual_report.summary.here_it_is": "Hona hemen zure {year}. urtearen bilduma:", - "annual_report.summary.highlighted_post.by_favourites": "egindako bidalketa gogokoena", - "annual_report.summary.highlighted_post.by_reblogs": "egindako bidalketa zabalduena", - "annual_report.summary.highlighted_post.by_replies": "erantzun gehien izan dituen bidalketa", - "annual_report.summary.highlighted_post.possessive": "{name}-(r)ena", + "annual_report.announcement.action_build": "Sortu nire Wrapstodon", + "annual_report.announcement.action_dismiss": "Ez, eskerrik asko", + "annual_report.announcement.action_view": "Ikusi nire Wrapstodon", + "annual_report.announcement.description": "Ezagutu azken urtean Mastodonen izan duzun parte-hartzeari buruzko informazio gehiago.", + "annual_report.announcement.title": "Hemen da {2025}(e)ko Wrapstodon", + "annual_report.nav_item.badge": "Berria", + "annual_report.shared_page.donate": "Egin dohaintza", + "annual_report.shared_page.footer": "Mastodonen taldeak {heart} sortua", + "annual_report.shared_page.footer_server_info": "{username}(e)k {domain} erabiltzen du, Mastodonek bultzatutako komunitate ugarietako bat.", + "annual_report.summary.archetype.booster.desc_public": "{name}(e)k promozionatzeko argitalpenen bila jarraitu zuen, beste sortzaile batzuk punteria perfektuarekin anplifikatuz.", + "annual_report.summary.archetype.booster.desc_self": "Bultzatu beharreko argitalpenen zelatan egon zinen, punteria perfektua zuten beste sortzaile batzuk anplifikatuz.", + "annual_report.summary.archetype.booster.name": "Arkularia", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.desc_public": "Badakigu {name} hor zegoela kanpoan, nonbait, Mastodonez gozatzen bere modu lasaian.", + "annual_report.summary.archetype.lurker.desc_self": "Badakigu hor zinela kanpoan, nonbait, Mastodonez gozatzen zure erara, isilean.", + "annual_report.summary.archetype.lurker.name": "Estoikoa", + "annual_report.summary.archetype.oracle.desc_public": "{name}(e)k erantzun baino argitalpen berri gehiago sortu zituen, eta horrek Mastodon fresko eta etorkizuneko ikuspegiarekin mantendu zuen.", + "annual_report.summary.archetype.oracle.desc_self": "Erantzun baino argitalpen berri gehiago sortu zenituen, eta horrek Mastodon fresko eta etorkizuneko ikuspegiarekin mantendu zuen.", + "annual_report.summary.archetype.oracle.name": "Orakulua", + "annual_report.summary.archetype.pollster.desc_public": "{name}(e)k inkesta gehiago sortu zituen beste argitalpen mota batzuek baino, eta horrek jakin-mina piztu zuen Mastodonen.", + "annual_report.summary.archetype.pollster.desc_self": "Beste argitalpen batzuk baino inkesta gehiago sortu zenituen, eta horrek jakin-mina piztu zuen Mastodonen.", + "annual_report.summary.archetype.pollster.name": "Ameslaria", + "annual_report.summary.archetype.replier.desc_public": "{name}(e)k maiz erantzuten zien beste pertsona batzuen argitalpenei, Mastodon polinizatuz eztabaida berriekin.", + "annual_report.summary.archetype.replier.desc_self": "Maiz erantzuten zenien beste pertsona batzuen argitalpenei, Mastodon polinizatuz eztabaida berriekin.", + "annual_report.summary.archetype.replier.name": "Tximeleta", + "annual_report.summary.archetype.reveal": "Erakutsi nire arketipoa", + "annual_report.summary.archetype.reveal_description": "Eskerrik asko Mastodonen parte izateagatik! Bada garaia jakiteko zer arketipo haragitu duzun {year}(e)an.", + "annual_report.summary.archetype.title_public": "{name}-(r)en arketipoa", + "annual_report.summary.archetype.title_self": "Zure arketipoa", + "annual_report.summary.close": "Itxi", + "annual_report.summary.copy_link": "Kopiatu esteka", + "annual_report.summary.followers.new_followers": "{count, plural, one {jarraitzaile berri} other {jarraitzaile berri}}", + "annual_report.summary.highlighted_post.boost_count": "Bidalketa honek {count, plural, one {bultzada 1 dauka} other {# bultzada dauzka}}.", + "annual_report.summary.highlighted_post.favourite_count": "Bidalketa honek {count, plural, one {gogoko 1 dauka} other {# gogoko dauzka}}.", + "annual_report.summary.highlighted_post.reply_count": "Bidalketa honek {count, plural, one {erantzun 1 dauka} other {# erantzun dauzka}}.", + "annual_report.summary.highlighted_post.title": "Bidalketa ospetsuena", "annual_report.summary.most_used_app.most_used_app": "app erabiliena", "annual_report.summary.most_used_hashtag.most_used_hashtag": "traola erabiliena", - "annual_report.summary.most_used_hashtag.none": "Bat ere ez", + "annual_report.summary.most_used_hashtag.used_count": "Traola hau {count, plural, one {bidalketa baten sartu zenuen} other {# bidalketatan sartu zenuen}}.", + "annual_report.summary.most_used_hashtag.used_count_public": "{name}(e)k traola hau {count, plural, one {bidalketa baten sartu zuen} other {# bidalketatan sartu zituen}}.", "annual_report.summary.new_posts.new_posts": "bidalketa berriak", "annual_report.summary.percentile.text": "Horrek jartzen zaitu top (e)an {domain} erabiltzaileen artean ", "annual_report.summary.percentile.we_wont_tell_bernie": "Bernieri ez diogu ezer esango ;)..", - "annual_report.summary.thanks": "Eskerrik asko Mastodonen parte izateagatik!", + "annual_report.summary.share_elsewhere": "Partekatu beste edonon", + "annual_report.summary.share_message": "{arketipoa} arketipoa daukat!", + "annual_report.summary.share_on_mastodon": "Partekatu Mastodonen", "attachments_list.unprocessed": "(prozesatu gabe)", "audio.hide": "Ezkutatu audioa", "block_modal.remote_users_caveat": "{domain} zerbitzariari zure erabakia errespeta dezan eskatuko diogu. Halere, araua beteko den ezin da bermatu, zerbitzari batzuk modu desberdinean kudeatzen baitituzte blokeoak. Baliteke argitalpen publikoak saioa hasi ez duten erabiltzaileentzat ikusgai egotea.", @@ -249,7 +276,7 @@ "confirmations.missing_alt_text.secondary": "Bidali edonola ere", "confirmations.missing_alt_text.title": "Testu alternatiboa gehitu?", "confirmations.mute.confirm": "Mututu", - "confirmations.private_quote_notify.cancel": "Ediziora bueltatu", + "confirmations.private_quote_notify.cancel": "Bueltatu ediziora", "confirmations.private_quote_notify.confirm": "Argitaratu bidalketa", "confirmations.private_quote_notify.do_not_show_again": "Ez erakutsi mezu hau berriro", "confirmations.private_quote_notify.message": "Aipatzen ari zaren pertsonak eta aipatutako besteek jakinarazpena jasoko dute eta zure sarrera ikusi ahalko dute, zure jarraitzaileak ez badira ere.", @@ -357,6 +384,7 @@ "empty_column.notification_requests": "Garbi-garbi! Ezertxo ere ez hemen. Jakinarazpenak jasotzen dituzunean, hemen agertuko dira zure ezarpenen arabera.", "empty_column.notifications": "Ez duzu jakinarazpenik oraindik. Jarri besteekin harremanetan elkarrizketa abiatzeko.", "empty_column.public": "Ez dago ezer hemen! Idatzi zerbait publikoki edo jarraitu eskuz beste zerbitzari batzuetako erabiltzaileei hau betetzen joateko", + "error.no_hashtag_feed_access": "Egin bat edo hasi saioa etiketa hau ikusi eta jarraitzeko.", "error.unexpected_crash.explanation": "Gure kodean arazoren bat dela eta, edo nabigatzailearekin bateragarritasun arazoren bat dela eta, orri hau ezin izan da ongi bistaratu.", "error.unexpected_crash.explanation_addons": "Ezin izan da orria behar bezala bistaratu. Errore honen jatorria nabigatzailearen gehigarri batean edo itzulpen automatikoko tresnetan dago ziur aski.", "error.unexpected_crash.next_steps": "Saiatu orria berritzen. Horrek ez badu laguntzen, agian Mastodon erabiltzeko aukera duzu oraindik ere beste nabigatzaile bat edo aplikazio natibo bat erabilita.", @@ -413,6 +441,8 @@ "follow_suggestions.who_to_follow": "Zein jarraitu", "followed_tags": "Jarraitutako traolak", "footer.about": "Honi buruz", + "footer.about_mastodon": "Mastodoni buruz", + "footer.about_server": "{domain}-i buruz", "footer.about_this_server": "Honi buruz", "footer.directory": "Profil-direktorioa", "footer.get_app": "Eskuratu aplikazioa", @@ -515,6 +545,7 @@ "keyboard_shortcuts.toggle_hidden": "testua erakustea/ezkutatzea abisu baten atzean", "keyboard_shortcuts.toggle_sensitivity": "multimedia erakutsi/ezkutatzeko", "keyboard_shortcuts.toot": "Hasi bidalketa berri bat", + "keyboard_shortcuts.top": "Mugitu zerrendaren hasierara", "keyboard_shortcuts.translate": "bidalketa itzultzeko", "keyboard_shortcuts.unfocus": "testua konposatzeko area / bilaketatik fokua kentzea", "keyboard_shortcuts.up": "zerrendan gora mugitzea", @@ -760,7 +791,7 @@ "privacy.quote.limited": "{visibility}, aipuak mugatuta", "privacy.unlisted.additional": "Aukera honek publiko modua bezala funtzionatzen du, baina argitalpena ez da agertuko zuzeneko jarioetan edo traoletan, \"Arakatu\" atalean edo Mastodonen bilaketan, nahiz eta kontua zabaltzeko onartu duzun.", "privacy.unlisted.long": "Ezkutatuta Mastodon bilaketen emaitzetatik, joeretatik, eta denbora-lerro publikoetatik", - "privacy.unlisted.short": "Deiadar urrikoa", + "privacy.unlisted.short": "Ikusgarritasun mugatua", "privacy_policy.last_updated": "Azkenengo eguneraketa {date}", "privacy_policy.title": "Pribatutasun politika", "quote_error.edit": "Aipuak ezin dira gehitu bidalketa bat editatzean.", @@ -903,6 +934,7 @@ "status.edited_x_times": "{count, plural, one {behin} other {{count} aldiz}} editatua", "status.embed": "Lortu txertatzeko kodea", "status.favourite": "Gogokoa", + "status.favourites_count": "{count, plural, one {{counter} gogoko} other {{counter} gogoko}}", "status.filter": "Iragazi bidalketa hau", "status.history.created": "{name} erabiltzaileak sortua {date}", "status.history.edited": "{name} erabiltzaileak editatua {date}", @@ -937,12 +969,14 @@ "status.quotes.empty": "Momentuz inork ez du bidalketa hau aipatu. Norbaitek eginez gero, hemen agertuko da.", "status.quotes.local_other_disclaimer": "Egileak errefusatutako aipuak ez dira erakutsiko.", "status.quotes.remote_other_disclaimer": "{domain} zerbitzariko aipuak baino ez daude bermatuta hemen. Egileak errefusatutako aipuak ez dira erakutsiko.", + "status.quotes_count": "{count, plural, one {{counter} aipu} other {{counter} aipu}}", "status.read_more": "Irakurri gehiago", "status.reblog": "Bultzada", "status.reblog_or_quote": "Bultzatu edo aipatu", "status.reblog_private": "Partekatu berriz zure jarraitzaileekin", "status.reblogged_by": "{name}(r)en bultzada", "status.reblogs.empty": "Inork ez dio bultzada eman bidalketa honi oraindik. Inork egiten badu, hemen agertuko da.", + "status.reblogs_count": "{count, plural, one {{counter} bultzaeda} other {{counter} bultzaeda}}", "status.redraft": "Ezabatu eta berridatzi", "status.remove_bookmark": "Kendu laster-marka", "status.remove_favourite": "Kendu gogokoetatik", diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json index 7eda0c0a923ad5..7684a328fbb5ca 100644 --- a/app/javascript/mastodon/locales/fa.json +++ b/app/javascript/mastodon/locales/fa.json @@ -117,26 +117,12 @@ "annual_report.announcement.action_view": "دیدن خلاصهٔ ماستودونم", "annual_report.announcement.description": "کشف بیش‌تر دربارهٔ درگیریتان روی ماستودون در سال گذشته.", "annual_report.announcement.title": "خلاصهٔ ماستودون {year} این‌جاست", - "annual_report.summary.archetype.booster": "باحال‌یاب", - "annual_report.summary.archetype.lurker": "کم‌پیدا", - "annual_report.summary.archetype.oracle": "غیب‌گو", - "annual_report.summary.archetype.pollster": "نظرسنج", - "annual_report.summary.archetype.replier": "پاسخ‌گو", - "annual_report.summary.followers.followers": "دنبال کننده", - "annual_report.summary.followers.total": "در مجموع {count}", - "annual_report.summary.here_it_is": "بازبینی {year} تان:", - "annual_report.summary.highlighted_post.by_favourites": "پرپسندترین فرسته", - "annual_report.summary.highlighted_post.by_reblogs": "پرتقویت‌ترین فرسته", - "annual_report.summary.highlighted_post.by_replies": "پرپاسخ‌ترین فرسته", - "annual_report.summary.highlighted_post.possessive": "{name}", "annual_report.summary.most_used_app.most_used_app": "پراستفاده‌ترین کاره", "annual_report.summary.most_used_hashtag.most_used_hashtag": "پراستفاده‌ترین برچسب", - "annual_report.summary.most_used_hashtag.none": "هیچ‌کدام", "annual_report.summary.new_posts.new_posts": "فرستهٔ جدید", "annual_report.summary.percentile.text": "بین کاربران {domain} جزوبرتر هستید.", "annual_report.summary.percentile.we_wont_tell_bernie": "به برنی خبر نمی‌دهیم.", "annual_report.summary.share_message": "من کهن‌الگوی {archetype} را گرفتم!", - "annual_report.summary.thanks": "سپاس که بخشی از ماستودون هستید!", "attachments_list.unprocessed": "(پردازش نشده)", "audio.hide": "نهفتن صدا", "block_modal.remote_users_caveat": "از کارساز {domain} خواهیم خواست که به تصمیمتان احترام بگذارد. با این حال تضمینی برای رعایتش وجود ندارد؛ زیرا برخی کارسازها ممکن است مسدودی را متفاوت مدیریت کنند. ممکن است فرسته‌های عمومی همچنان برای کاربران وارد نشده نمایان باشند.", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index e811ba70d19e36..c0cd9a77cd5a9b 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -114,28 +114,51 @@ "alt_text_modal.done": "Valmis", "announcement.announcement": "Tiedote", "annual_report.announcement.action_build": "Koosta oma Wrapstodon", + "annual_report.announcement.action_dismiss": "Ei kiitos", "annual_report.announcement.action_view": "Näytä oma Wrapstodon", "annual_report.announcement.description": "Tutustu toimintaasi Mastodonissa kuluneen vuoden aikana.", "annual_report.announcement.title": "Wrapstodon {year} on täällä", - "annual_report.summary.archetype.booster": "Tehostaja", - "annual_report.summary.archetype.lurker": "Lymyilijä", - "annual_report.summary.archetype.oracle": "Oraakkeli", - "annual_report.summary.archetype.pollster": "Mielipidetutkija", - "annual_report.summary.archetype.replier": "Sosiaalinen perhonen", - "annual_report.summary.followers.followers": "seuraajaa", - "annual_report.summary.followers.total": "{count} yhteensä", - "annual_report.summary.here_it_is": "Tässä on katsaus vuoteesi {year}:", - "annual_report.summary.highlighted_post.by_favourites": "suosikkeihin lisätyin julkaisu", - "annual_report.summary.highlighted_post.by_reblogs": "tehostetuin julkaisu", - "annual_report.summary.highlighted_post.by_replies": "julkaisu, jolla on eniten vastauksia", - "annual_report.summary.highlighted_post.possessive": "Käyttäjän {name}", + "annual_report.nav_item.badge": "Uusi", + "annual_report.shared_page.donate": "Lahjoita", + "annual_report.shared_page.footer": "Luotu {heart}:lla Mastodon-tiimin toimesta", + "annual_report.shared_page.footer_server_info": "{username} käyttää palvelinta {domain}, yhtä monista Mastodonin tarjoamista yhteisöistä.", + "annual_report.summary.archetype.booster.desc_public": "{name} pysyi valppaana tehostettavien julkaisujen suhteen, vahvistaen muita luojia täydellisellä tähtäyksellä.", + "annual_report.summary.archetype.booster.desc_self": "Pysyit valppaana tehostettavien julkaisujen suhteen, vahvistaen muita luojia täydellisellä tähtäyksellä.", + "annual_report.summary.archetype.booster.name": "Jousimies", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.desc_public": "Tiedämme, että {name} oli siellä jossakin, nauttien Mastodonista omalla hiljaisella tavallaan.", + "annual_report.summary.archetype.lurker.desc_self": "Tiedämme, että olit siellä jossakin, nauttien Mastodonista omalla hiljaisella tavallasi.", + "annual_report.summary.archetype.lurker.name": "Stoalainen", + "annual_report.summary.archetype.oracle.desc_public": "{name} loi enemmän julkaisuja kuin vastauksia, siten pitäen Mastodonin tuoreena ja valmiina tulevaisuuteen.", + "annual_report.summary.archetype.oracle.desc_self": "Loit enemmän julkaisuja kuin vastauksia, siten pitäen Mastodonin tuoreena ja valmiina tulevaisuuteen.", + "annual_report.summary.archetype.oracle.name": "Oraakkeli", + "annual_report.summary.archetype.pollster.desc_public": "{name} loi enemmän äänestyksiä kuin muita julkaisutyyppejä, siten herättäen uteliaisuutta Mastodonissa.", + "annual_report.summary.archetype.pollster.desc_self": "Loit enemmän äänestyksiä kuin muita julkaisutyyppejä, siten herättäen uteliaisuutta Mastodonissa.", + "annual_report.summary.archetype.pollster.name": "Ihmettelijä", + "annual_report.summary.archetype.replier.desc_public": "{name} vastasi säännöllisesti toisten julkaisuihin, siten pölyttäen Mastodonia uusilla keskusteluilla.", + "annual_report.summary.archetype.replier.desc_self": "Vastasit säännöllisesti toisten julkaisuihin, siten pölyttäen Mastodonia uusilla keskusteluilla.", + "annual_report.summary.archetype.replier.name": "Perhonen", + "annual_report.summary.archetype.reveal": "Paljasta arkkityyppini", + "annual_report.summary.archetype.reveal_description": "Kiitos, että olet osa Mastodonia! Aika selvittää, minkä arkkityypin ruumiillistuma olit vuonna {year}.", + "annual_report.summary.archetype.title_public": "Käyttäjän {name} arkkityyppi", + "annual_report.summary.archetype.title_self": "Arkkityyppisi", + "annual_report.summary.close": "Sulje", + "annual_report.summary.copy_link": "Kopioi linkki", + "annual_report.summary.followers.new_followers": "{count, plural, one {uusi seuraaja} other {uutta seuraajaa}}", + "annual_report.summary.highlighted_post.boost_count": "Tätä julkaisua tehostettiin {count, plural, one {kerran} other {# kertaa}}.", + "annual_report.summary.highlighted_post.favourite_count": "Tämä julkaisu lisättiin suosikkeihin {count, plural, one {kerran} other {# kertaa}}.", + "annual_report.summary.highlighted_post.reply_count": "Tämä julkaisu sai {count, plural, one {1 vastauksen} other {# vastausta}}.", + "annual_report.summary.highlighted_post.title": "Suosituin julkaisu", "annual_report.summary.most_used_app.most_used_app": "käytetyin sovellus", "annual_report.summary.most_used_hashtag.most_used_hashtag": "käytetyin aihetunniste", - "annual_report.summary.most_used_hashtag.none": "Ei mitään", + "annual_report.summary.most_used_hashtag.used_count": "Sisällytit tämän aihetunnisteen {count, plural, one {1 julkaisuun} other {# julkaisuun}}.", + "annual_report.summary.most_used_hashtag.used_count_public": "{name} sisällytti tämän aihetunnisteen {count, plural, one {1 julkaisuun} other {# julkaisuun}}.", "annual_report.summary.new_posts.new_posts": "uutta julkaisua", "annual_report.summary.percentile.text": "Olet osa huippujoukkoa, johon kuuluu{domain}-käyttäjistä.", "annual_report.summary.percentile.we_wont_tell_bernie": "Emme kerro Bernie Sandersille.", - "annual_report.summary.thanks": "Kiitos, että olet osa Mastodonia!", + "annual_report.summary.share_elsewhere": "Jaa muualla", + "annual_report.summary.share_message": "Arkkityyppini on {archetype}!", + "annual_report.summary.share_on_mastodon": "Jaa Mastodonissa", "attachments_list.unprocessed": "(käsittelemätön)", "audio.hide": "Piilota ääni", "block_modal.remote_users_caveat": "Pyydämme palvelinta {domain} kunnioittamaan päätöstäsi. Myötämielisyyttä ei kuitenkaan taata, koska jotkin palvelimet voivat käsitellä estoja eri tavalla. Julkiset julkaisut voivat silti näkyä kirjautumattomille käyttäjille.", @@ -418,6 +441,8 @@ "follow_suggestions.who_to_follow": "Ehdotuksia seurattavaksi", "followed_tags": "Seurattavat aihetunnisteet", "footer.about": "Tietoja", + "footer.about_mastodon": "Tietoja Mastodonista", + "footer.about_server": "Tietoja palvelimesta {domain}", "footer.about_this_server": "Tietoja", "footer.directory": "Profiilihakemisto", "footer.get_app": "Hanki sovellus", diff --git a/app/javascript/mastodon/locales/fo.json b/app/javascript/mastodon/locales/fo.json index 21f03cfb5a49e9..e87ba3cead3e55 100644 --- a/app/javascript/mastodon/locales/fo.json +++ b/app/javascript/mastodon/locales/fo.json @@ -114,29 +114,51 @@ "alt_text_modal.done": "Liðugt", "announcement.announcement": "Kunngerð", "annual_report.announcement.action_build": "Ger mítt Wrapstodon", + "annual_report.announcement.action_dismiss": "Nei takk", "annual_report.announcement.action_view": "Vís mítt Wrapstodon", "annual_report.announcement.description": "Fá meira at vita um títt virksemi á Mastodon seinasta árið.", "annual_report.announcement.title": "Wrapstodon {year} er komið", - "annual_report.summary.archetype.booster": "Kuli jagarin", - "annual_report.summary.archetype.lurker": "Lúrarin", - "annual_report.summary.archetype.oracle": "Oraklið", - "annual_report.summary.archetype.pollster": "Spyrjarin", - "annual_report.summary.archetype.replier": "Sosiali firvaldurin", - "annual_report.summary.followers.followers": "fylgjarar", - "annual_report.summary.followers.total": "{count} íalt", - "annual_report.summary.here_it_is": "Her er ein samandráttur av {year}:", - "annual_report.summary.highlighted_post.by_favourites": "mest dámdi postur", - "annual_report.summary.highlighted_post.by_reblogs": "oftast stimbraði postur", - "annual_report.summary.highlighted_post.by_replies": "postur við flestum svarum", - "annual_report.summary.highlighted_post.possessive": "hjá {name}", + "annual_report.nav_item.badge": "Nýtt", + "annual_report.shared_page.donate": "Stuðla", + "annual_report.shared_page.footer": "Gjørt við {heart} av Mastodon toyminum", + "annual_report.shared_page.footer_server_info": "{username} brúkar {domain}, ein av fleiri felagsskapum, sum er drivin av Mastodon.", + "annual_report.summary.archetype.booster.desc_public": "{name} helt fram at veiða postar at lyfta, og styrkti aðrar skaparar við perfektum sikti.", + "annual_report.summary.archetype.booster.desc_self": "Tú helt fram at veiða postar at lyfta, og styrkti aðrar skaparar við perfektum sikti.", + "annual_report.summary.archetype.booster.name": "Bogaskjúttin", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.desc_public": "Vit vita, at {name} var úti har onkustaðni og neyt Mastodon uppá sín egna stilla máta.", + "annual_report.summary.archetype.lurker.desc_self": "Vit vita, at tú var úti har onkustaðni og neyt Mastodon uppá tín egna stilla máta.", + "annual_report.summary.archetype.lurker.name": "Stóikarin", + "annual_report.summary.archetype.oracle.desc_public": "{name} gjørdi fleiri nýggjar postar enn svar og helt Mastodon feskt og framtíðarrættað.", + "annual_report.summary.archetype.oracle.desc_self": "Tú gjørdi fleiri nýggjar postar enn svar og helt Mastodon feskt og framtíðarrættað.", + "annual_report.summary.archetype.oracle.name": "Oraklið", + "annual_report.summary.archetype.pollster.desc_public": "{name} stovnaði fleiri atkvøðugreiðslur enn onnur sløg av postum og dyrkaði forvitni á Mastodon.", + "annual_report.summary.archetype.pollster.desc_self": "Tú stovnaði fleiri atkvøðugreiðslur enn onnur sløg av postum og dyrkaði forvitni á Mastodon.", + "annual_report.summary.archetype.pollster.name": "Undrandi", + "annual_report.summary.archetype.replier.desc_public": "{name} svaraði ofta til postar hjá øðrum og ríkaði Mastodon við nýggjum kjaki.", + "annual_report.summary.archetype.replier.desc_self": "Tú svaraði ofta til postar hjá øðrum og ríkaði Mastodon við nýggjum kjaki.", + "annual_report.summary.archetype.replier.name": "Firvaldurin", + "annual_report.summary.archetype.reveal": "Avdúka mítt frumsnið", + "annual_report.summary.archetype.reveal_description": "Takk fyri at tú er partur av Mastodon! Nú er tíðin komin at finna útav hvat frumsnið tú fall inn í í {year}.", + "annual_report.summary.archetype.title_public": "Frumsniðið hjá {name}", + "annual_report.summary.archetype.title_self": "Títt frumsnið", + "annual_report.summary.close": "Lat aftur", + "annual_report.summary.copy_link": "Avrita leinki", + "annual_report.summary.followers.new_followers": "{count, plural, one {{counter} nýggjur fylgjari} other {{counter} nýggir fylgjarar}}", + "annual_report.summary.highlighted_post.boost_count": "Hesin posturin var lyftur {count, plural, one {eina ferð} other {# ferðir}}.", + "annual_report.summary.highlighted_post.favourite_count": "Hesin posturin var yndismerktur {count, plural, one {eina ferð} other {# ferðir}}.", + "annual_report.summary.highlighted_post.reply_count": "Hesin posturin fekk {count, plural, one {eitt svar} other {# svar}}.", + "annual_report.summary.highlighted_post.title": "Best umtókti postur", "annual_report.summary.most_used_app.most_used_app": "mest brúkta app", "annual_report.summary.most_used_hashtag.most_used_hashtag": "mest brúkta frámerki", - "annual_report.summary.most_used_hashtag.none": "Einki", + "annual_report.summary.most_used_hashtag.used_count": "Tú brúkti hetta frámerkið í {count, plural, one {einum posti} other {# postum}}.", + "annual_report.summary.most_used_hashtag.used_count_public": "{name} brúkti hetta frámerkið í {count, plural, one {einum posti} other {# postum}}.", "annual_report.summary.new_posts.new_posts": "nýggir postar", "annual_report.summary.percentile.text": "Tað fær teg í toppav {domain} brúkarum.", "annual_report.summary.percentile.we_wont_tell_bernie": "Vit fara ikki at fortelja Bernie tað.", + "annual_report.summary.share_elsewhere": "Deil aðrastaðni", "annual_report.summary.share_message": "Eg fekk {archetype} frumsniðið!", - "annual_report.summary.thanks": "Takk fyri at tú er partur av Mastodon!", + "annual_report.summary.share_on_mastodon": "Deil á Mastodon", "attachments_list.unprocessed": "(óviðgjørt)", "audio.hide": "Fjal ljóð", "block_modal.remote_users_caveat": "Vit biðja ambætaran {domain} virða tína avgerð. Kortini er eingin vissa um samsvar, av tí at fleiri ambætarar handfara blokkar ymiskt. Almennir postar kunnu framvegis vera sjónligir fyri brúkarar, sum ikki eru innritaðir.", @@ -419,6 +441,8 @@ "follow_suggestions.who_to_follow": "Hvørji tú átti at fylgt", "followed_tags": "Fylgd frámerki", "footer.about": "Um", + "footer.about_mastodon": "Um Mastodon", + "footer.about_server": "Um {domain}", "footer.about_this_server": "Um", "footer.directory": "Vangaskrá", "footer.get_app": "Heinta appina", diff --git a/app/javascript/mastodon/locales/fr-CA.json b/app/javascript/mastodon/locales/fr-CA.json index 1975ce0c89e49a..95b8efe078ba78 100644 --- a/app/javascript/mastodon/locales/fr-CA.json +++ b/app/javascript/mastodon/locales/fr-CA.json @@ -114,29 +114,51 @@ "alt_text_modal.done": "Terminé", "announcement.announcement": "Annonce", "annual_report.announcement.action_build": "Générer mon Wrapstodon", + "annual_report.announcement.action_dismiss": "Non merci", "annual_report.announcement.action_view": "Voir mon Wrapstodon", "annual_report.announcement.description": "En découvrir plus concernant votre activité sur Mastodon pour l'année écoulée.", "annual_report.announcement.title": "Le Wrapstodon {year} est arrivé", - "annual_report.summary.archetype.booster": "Le chasseur de sang-froid", - "annual_report.summary.archetype.lurker": "Le faucheur", - "annual_report.summary.archetype.oracle": "L’oracle", - "annual_report.summary.archetype.pollster": "Le sondeur", - "annual_report.summary.archetype.replier": "Le papillon social", - "annual_report.summary.followers.followers": "abonné·e·s", - "annual_report.summary.followers.total": "{count} au total", - "annual_report.summary.here_it_is": "Voici votre récap de {year} :", - "annual_report.summary.highlighted_post.by_favourites": "post le plus aimé", - "annual_report.summary.highlighted_post.by_reblogs": "post le plus boosté", - "annual_report.summary.highlighted_post.by_replies": "post avec le plus de réponses", - "annual_report.summary.highlighted_post.possessive": "{name}'s", + "annual_report.nav_item.badge": "Nouveau", + "annual_report.shared_page.donate": "Faire un don", + "annual_report.shared_page.footer": "Généré avec {heart} par l'équipe de Mastodon", + "annual_report.shared_page.footer_server_info": "{username} utilise {domain}, l'une des nombreuses communautés propulsées par Mastodon.", + "annual_report.summary.archetype.booster.desc_public": "{name} fut à l’affût des messages à partager, ciblant avec perfection les créatrices et créateurs à amplifier.", + "annual_report.summary.archetype.booster.desc_self": "Vous avez été à l'affût des messages à partager, ciblant avec perfection les créatrices et créateurs à amplifier.", + "annual_report.summary.archetype.booster.name": "Archer, archère", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.desc_public": "Nous savons que {name} a été là, quelque part, profitant calmement de Mastodon à sa manière.", + "annual_report.summary.archetype.lurker.desc_self": "Nous savons que vous avez été là, quelque part, profitant calmement de Mastodon à votre manière.", + "annual_report.summary.archetype.lurker.name": "Stoïque", + "annual_report.summary.archetype.oracle.desc_public": "{name} publia plus de nouveaux messages que de réponses, contribuant à garder Mastodon frais et tourné vers l'avenir.", + "annual_report.summary.archetype.oracle.desc_self": "Vous avez publié plus de nouveaux messages que de réponses, contribuant à garder Mastodon frais et tourné vers l'avenir.", + "annual_report.summary.archetype.oracle.name": "Oracle", + "annual_report.summary.archetype.pollster.desc_public": "{name} publia plus de sondages que n'importe quel autre type de message, cultivant la curiosité sur Mastodon.", + "annual_report.summary.archetype.pollster.desc_self": "Vous avez publié plus de sondages que n'importe quel autre type de message, cultivant la curiosité sur Mastodon.", + "annual_report.summary.archetype.pollster.name": "Curieux, curieuse", + "annual_report.summary.archetype.replier.desc_public": "{name} répliqua fréquemment aux messages des autres, pollinisant Mastodon de nouvelles discussions.", + "annual_report.summary.archetype.replier.desc_self": "Vous avez fréquemment répliqué aux messages des autres, pollinisant Mastodon de nouvelles discussions.", + "annual_report.summary.archetype.replier.name": "Papillon", + "annual_report.summary.archetype.reveal": "Révéler mon archétype", + "annual_report.summary.archetype.reveal_description": "Merci de faire partie de Mastodon ! Il est temps de découvrir quel archétype vous avez incarné en {year}.", + "annual_report.summary.archetype.title_public": "L'archétype de {name}", + "annual_report.summary.archetype.title_self": "Votre archétype", + "annual_report.summary.close": "Fermer", + "annual_report.summary.copy_link": "Copier le lien", + "annual_report.summary.followers.new_followers": "{count, plural, one {nouvel·le abonné·e} other {nouveaux abonné·e·s}}", + "annual_report.summary.highlighted_post.boost_count": "Ce message a été partagé {count, plural, one {une fois} other {# fois}}.", + "annual_report.summary.highlighted_post.favourite_count": "Ce message a été mis en favoris {count, plural, one {une fois} other {# fois}}.", + "annual_report.summary.highlighted_post.reply_count": "Ce message a reçu {count, plural, one {une réponse} other {# réponses}}.", + "annual_report.summary.highlighted_post.title": "Message le plus populaire", "annual_report.summary.most_used_app.most_used_app": "appli la plus utilisée", "annual_report.summary.most_used_hashtag.most_used_hashtag": "hashtag le plus utilisé", - "annual_report.summary.most_used_hashtag.none": "Aucun", + "annual_report.summary.most_used_hashtag.used_count": "Vous avez utilisé ce hashtag dans {count, plural, one {un message} other {# messages}}.", + "annual_report.summary.most_used_hashtag.used_count_public": "{name} a inclus ce hashtag dans {count, plural, one {un message} other {# messages}}.", "annual_report.summary.new_posts.new_posts": "nouveaux messages", "annual_report.summary.percentile.text": "Cela vous place dans le topdes utilisateurs de {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "Nous ne le dirons pas à Bernie.", + "annual_report.summary.share_elsewhere": "Partager ailleurs", "annual_report.summary.share_message": "J’ai obtenu l’archétype {archetype} !", - "annual_report.summary.thanks": "Merci de faire partie de Mastodon!", + "annual_report.summary.share_on_mastodon": "Partager sur Mastodon", "attachments_list.unprocessed": "(non traité)", "audio.hide": "Masquer l'audio", "block_modal.remote_users_caveat": "Nous allons demander au serveur {domain} de respecter votre décision. Cependant, ce respect n'est pas garanti, car certains serveurs peuvent gérer différemment les blocages. Les messages publics peuvent rester visibles par les utilisateur·rice·s non connecté·e·s.", @@ -419,6 +441,8 @@ "follow_suggestions.who_to_follow": "Qui suivre", "followed_tags": "Hashtags suivis", "footer.about": "À propos", + "footer.about_mastodon": "À propos de Mastodon", + "footer.about_server": "À propos de {domain}", "footer.about_this_server": "À propos", "footer.directory": "Annuaire des profils", "footer.get_app": "Télécharger l’application", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index 2ad548d9a4a035..2ef5c9acb40b17 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -114,29 +114,51 @@ "alt_text_modal.done": "Terminé", "announcement.announcement": "Annonce", "annual_report.announcement.action_build": "Générer mon Wrapstodon", + "annual_report.announcement.action_dismiss": "Non merci", "annual_report.announcement.action_view": "Voir mon Wrapstodon", "annual_report.announcement.description": "En découvrir plus concernant votre activité sur Mastodon pour l'année écoulée.", "annual_report.announcement.title": "Le Wrapstodon {year} est arrivé", - "annual_report.summary.archetype.booster": "Le chasseur de sang-froid", - "annual_report.summary.archetype.lurker": "Le faucheur", - "annual_report.summary.archetype.oracle": "L’oracle", - "annual_report.summary.archetype.pollster": "Le sondeur", - "annual_report.summary.archetype.replier": "Le papillon social", - "annual_report.summary.followers.followers": "abonné·e·s", - "annual_report.summary.followers.total": "{count} au total", - "annual_report.summary.here_it_is": "Voici votre récap de {year} :", - "annual_report.summary.highlighted_post.by_favourites": "post le plus aimé", - "annual_report.summary.highlighted_post.by_reblogs": "post le plus boosté", - "annual_report.summary.highlighted_post.by_replies": "post avec le plus de réponses", - "annual_report.summary.highlighted_post.possessive": "{name}'s", + "annual_report.nav_item.badge": "Nouveau", + "annual_report.shared_page.donate": "Faire un don", + "annual_report.shared_page.footer": "Généré avec {heart} par l'équipe de Mastodon", + "annual_report.shared_page.footer_server_info": "{username} utilise {domain}, l'une des nombreuses communautés propulsées par Mastodon.", + "annual_report.summary.archetype.booster.desc_public": "{name} fut à l’affût des messages à partager, ciblant avec perfection les créatrices et créateurs à amplifier.", + "annual_report.summary.archetype.booster.desc_self": "Vous avez été à l'affût des messages à partager, ciblant avec perfection les créatrices et créateurs à amplifier.", + "annual_report.summary.archetype.booster.name": "Archer, archère", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.desc_public": "Nous savons que {name} a été là, quelque part, profitant calmement de Mastodon à sa manière.", + "annual_report.summary.archetype.lurker.desc_self": "Nous savons que vous avez été là, quelque part, profitant calmement de Mastodon à votre manière.", + "annual_report.summary.archetype.lurker.name": "Stoïque", + "annual_report.summary.archetype.oracle.desc_public": "{name} publia plus de nouveaux messages que de réponses, contribuant à garder Mastodon frais et tourné vers l'avenir.", + "annual_report.summary.archetype.oracle.desc_self": "Vous avez publié plus de nouveaux messages que de réponses, contribuant à garder Mastodon frais et tourné vers l'avenir.", + "annual_report.summary.archetype.oracle.name": "Oracle", + "annual_report.summary.archetype.pollster.desc_public": "{name} publia plus de sondages que n'importe quel autre type de message, cultivant la curiosité sur Mastodon.", + "annual_report.summary.archetype.pollster.desc_self": "Vous avez publié plus de sondages que n'importe quel autre type de message, cultivant la curiosité sur Mastodon.", + "annual_report.summary.archetype.pollster.name": "Curieux, curieuse", + "annual_report.summary.archetype.replier.desc_public": "{name} répliqua fréquemment aux messages des autres, pollinisant Mastodon de nouvelles discussions.", + "annual_report.summary.archetype.replier.desc_self": "Vous avez fréquemment répliqué aux messages des autres, pollinisant Mastodon de nouvelles discussions.", + "annual_report.summary.archetype.replier.name": "Papillon", + "annual_report.summary.archetype.reveal": "Révéler mon archétype", + "annual_report.summary.archetype.reveal_description": "Merci de faire partie de Mastodon ! Il est temps de découvrir quel archétype vous avez incarné en {year}.", + "annual_report.summary.archetype.title_public": "L'archétype de {name}", + "annual_report.summary.archetype.title_self": "Votre archétype", + "annual_report.summary.close": "Fermer", + "annual_report.summary.copy_link": "Copier le lien", + "annual_report.summary.followers.new_followers": "{count, plural, one {nouvel·le abonné·e} other {nouveaux abonné·e·s}}", + "annual_report.summary.highlighted_post.boost_count": "Ce message a été partagé {count, plural, one {une fois} other {# fois}}.", + "annual_report.summary.highlighted_post.favourite_count": "Ce message a été mis en favoris {count, plural, one {une fois} other {# fois}}.", + "annual_report.summary.highlighted_post.reply_count": "Ce message a reçu {count, plural, one {une réponse} other {# réponses}}.", + "annual_report.summary.highlighted_post.title": "Message le plus populaire", "annual_report.summary.most_used_app.most_used_app": "appli la plus utilisée", "annual_report.summary.most_used_hashtag.most_used_hashtag": "hashtag le plus utilisé", - "annual_report.summary.most_used_hashtag.none": "Aucun", + "annual_report.summary.most_used_hashtag.used_count": "Vous avez utilisé ce hashtag dans {count, plural, one {un message} other {# messages}}.", + "annual_report.summary.most_used_hashtag.used_count_public": "{name} a inclus ce hashtag dans {count, plural, one {un message} other {# messages}}.", "annual_report.summary.new_posts.new_posts": "nouveaux messages", "annual_report.summary.percentile.text": "Cela vous place dans le topdes utilisateurs de {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "Nous ne le dirons pas à Bernie.", + "annual_report.summary.share_elsewhere": "Partager ailleurs", "annual_report.summary.share_message": "J’ai obtenu l’archétype {archetype} !", - "annual_report.summary.thanks": "Merci de faire partie de Mastodon!", + "annual_report.summary.share_on_mastodon": "Partager sur Mastodon", "attachments_list.unprocessed": "(non traité)", "audio.hide": "Masquer l'audio", "block_modal.remote_users_caveat": "Nous allons demander au serveur {domain} de respecter votre décision. Cependant, ce respect n'est pas garanti, car certains serveurs peuvent gérer différemment les blocages. Les messages publics peuvent rester visibles par les utilisateur·rice·s non connecté·e·s.", @@ -419,6 +441,8 @@ "follow_suggestions.who_to_follow": "Qui suivre", "followed_tags": "Hashtags suivis", "footer.about": "À propos", + "footer.about_mastodon": "À propos de Mastodon", + "footer.about_server": "À propos de {domain}", "footer.about_this_server": "À propos", "footer.directory": "Annuaire des profils", "footer.get_app": "Télécharger l’application", diff --git a/app/javascript/mastodon/locales/fy.json b/app/javascript/mastodon/locales/fy.json index e460b29de0349b..2a98f2978218b2 100644 --- a/app/javascript/mastodon/locales/fy.json +++ b/app/javascript/mastodon/locales/fy.json @@ -107,25 +107,11 @@ "alt_text_modal.describe_for_people_with_visual_impairments": "Beskriuw dit foar blinen en fisueel beheinde…", "alt_text_modal.done": "Klear", "announcement.announcement": "Oankundiging", - "annual_report.summary.archetype.booster": "De cool-hunter", - "annual_report.summary.archetype.lurker": "De lurker", - "annual_report.summary.archetype.oracle": "It orakel", - "annual_report.summary.archetype.pollster": "De opinypeiler", - "annual_report.summary.archetype.replier": "De sosjale flinter", - "annual_report.summary.followers.followers": "folgers", - "annual_report.summary.followers.total": "totaal {count}", - "annual_report.summary.here_it_is": "Jo jieroersjoch foar {year}:", - "annual_report.summary.highlighted_post.by_favourites": "berjocht mei de measte favoriten", - "annual_report.summary.highlighted_post.by_reblogs": "berjocht mei de measte boosts", - "annual_report.summary.highlighted_post.by_replies": "berjocht mei de measte reaksjes", - "annual_report.summary.highlighted_post.possessive": "{name}’s", "annual_report.summary.most_used_app.most_used_app": "meast brûkte app", "annual_report.summary.most_used_hashtag.most_used_hashtag": "meast brûkte hashtag", - "annual_report.summary.most_used_hashtag.none": "Gjin", "annual_report.summary.new_posts.new_posts": "nije berjochten", "annual_report.summary.percentile.text": "Hjirmei hearre jo ta de top fan {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "Wy sille Bernie neat fertelle.", - "annual_report.summary.thanks": "Tank dat jo part binne fan Mastodon!", "attachments_list.unprocessed": "(net ferwurke)", "audio.hide": "Audio ferstopje", "block_modal.remote_users_caveat": "Wy freegje de server {domain} om jo beslút te respektearjen. It neilibben hjirfan is echter net garandearre, omdat guon servers blokkaden oars ynterpretearje kinne. Iepenbiere berjochten binne mooglik noch hieltyd sichtber foar net-oanmelde brûkers.", diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json index 7172aaacf5794f..f1c51200ff30b3 100644 --- a/app/javascript/mastodon/locales/ga.json +++ b/app/javascript/mastodon/locales/ga.json @@ -114,29 +114,51 @@ "alt_text_modal.done": "Déanta", "announcement.announcement": "Fógra", "annual_report.announcement.action_build": "Tóg mo Wrapstodon", + "annual_report.announcement.action_dismiss": "Ní raibh maith agat", "annual_report.announcement.action_view": "Féach ar mo Wrapstodon", "annual_report.announcement.description": "Faigh tuilleadh eolais faoi do rannpháirtíocht ar Mastodon le bliain anuas.", "annual_report.announcement.title": "Tá Wrapstodon {year} tagtha", - "annual_report.summary.archetype.booster": "An sealgair fionnuar", - "annual_report.summary.archetype.lurker": "An lurker", - "annual_report.summary.archetype.oracle": "An oracal", - "annual_report.summary.archetype.pollster": "An pollaire", - "annual_report.summary.archetype.replier": "An féileacán sóisialta", - "annual_report.summary.followers.followers": "leanúna", - "annual_report.summary.followers.total": "{count} san iomlán", - "annual_report.summary.here_it_is": "Seo do {year} faoi athbhreithniú:", - "annual_report.summary.highlighted_post.by_favourites": "post is fearr leat", - "annual_report.summary.highlighted_post.by_reblogs": "post is treisithe", - "annual_report.summary.highlighted_post.by_replies": "post leis an líon is mó freagraí", - "annual_report.summary.highlighted_post.possessive": "{name}'s", + "annual_report.nav_item.badge": "Nua", + "annual_report.shared_page.donate": "Tabhair Síntiús", + "annual_report.shared_page.footer": "Gineadh le {heart} ag foireann Mastodon", + "annual_report.shared_page.footer_server_info": "Úsáideann {username} {domain}, ceann de go leor pobail atá faoi thiomáint ag Mastodon.", + "annual_report.summary.archetype.booster.desc_public": "D’fhan {name} ag cuardach postálacha le cur chun cinn, ag cur le cruthaitheoirí eile le cuspóir foirfe.", + "annual_report.summary.archetype.booster.desc_self": "D’fhan tú ag cuardach postálacha le borradh a chur fúthu, ag cur le cruthaitheoirí eile le cuspóir foirfe.", + "annual_report.summary.archetype.booster.name": "An Saighdeoir", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.desc_public": "Tá a fhios againn go raibh {name} amuigh ansin, áit éigin, ag baint taitnimh as Mastodon ar a mbealach ciúin féin.", + "annual_report.summary.archetype.lurker.desc_self": "Tá a fhios againn go raibh tú amuigh ansin, áit éigin, ag baint taitnimh as Mastodon ar do bhealach ciúin féin.", + "annual_report.summary.archetype.lurker.name": "An Stoiceach", + "annual_report.summary.archetype.oracle.desc_public": "Chruthaigh {name} níos mó postálacha nua ná freagraí, rud a choinnigh Mastodon úr agus dírithe ar an todhchaí.", + "annual_report.summary.archetype.oracle.desc_self": "Chruthaigh tú níos mó postálacha nua ná freagraí, rud a choinnigh Mastodon úr agus dírithe ar an todhchaí.", + "annual_report.summary.archetype.oracle.name": "An tOracal", + "annual_report.summary.archetype.pollster.desc_public": "Chruthaigh {name} níos mó pobalbhreitheanna ná cineálacha poist eile, rud a chothaigh fiosracht faoi Mastodon.", + "annual_report.summary.archetype.pollster.desc_self": "Chruthaigh tú níos mó pobalbhreitheanna ná cineálacha poist eile, rud a chothaigh fiosracht ar Mastodon.", + "annual_report.summary.archetype.pollster.name": "An tIontasóir", + "annual_report.summary.archetype.replier.desc_public": "Is minic a d’fhreagair {name} poist daoine eile, rud a spreag plé nua i Mastodon.", + "annual_report.summary.archetype.replier.desc_self": "Is minic a d’fhreagair tú poist daoine eile, ag maolú Mastodon le plé nua.", + "annual_report.summary.archetype.replier.name": "An Féileacán", + "annual_report.summary.archetype.reveal": "Nocht mo sheandálaíocht", + "annual_report.summary.archetype.reveal_description": "Go raibh maith agat as bheith mar chuid de Mastodon! Tá sé in am a fháil amach cén t-archetíopa a léirigh tú i {year}.", + "annual_report.summary.archetype.title_public": "Seanchineál {name}", + "annual_report.summary.archetype.title_self": "Do sheandálaíocht", + "annual_report.summary.close": "Dún", + "annual_report.summary.copy_link": "Cóipeáil nasc", + "annual_report.summary.followers.new_followers": "{count, plural, one {leantóir nua} two {leantóirí nua} few {leantóirí nua} many {leantóirí nua} other {leantóirí nua}}", + "annual_report.summary.highlighted_post.boost_count": "Borradh a tugadh faoin bpost seo {count, plural, one {uair amháin} two {# uaire} few {# uaire} many {# uaire} other {# uaire}}.", + "annual_report.summary.highlighted_post.favourite_count": "Cuireadh an post seo leis na ceanáin {count, plural, one {uair amháin} two {# uaire} few {# uaire} many {# uaire} other {# uaire}}.", + "annual_report.summary.highlighted_post.reply_count": "Fuair ​​an post seo {count, plural, one {freagra amháin} two {# freagraí} few {# freagraí} many {# freagraí} other {# freagraí}}.", + "annual_report.summary.highlighted_post.title": "An post is mó tóir", "annual_report.summary.most_used_app.most_used_app": "aip is mó a úsáidtear", "annual_report.summary.most_used_hashtag.most_used_hashtag": "hashtag is mó a úsáidtear", - "annual_report.summary.most_used_hashtag.none": "Dada", + "annual_report.summary.most_used_hashtag.used_count": "Chuir tú an haischlib seo i {count, plural, one {post amháin} two {# poist} few {# poist} many {# poist} other {# poist}}.", + "annual_report.summary.most_used_hashtag.used_count_public": "Chuir {name} an haischlib seo i {count, plural, one {post amháin} two {# poist} few {# poist} many {# poist} other {# poist}}.", "annual_report.summary.new_posts.new_posts": "postanna nua", "annual_report.summary.percentile.text": "Cuireann sé sin i mbarr úsáideoirí {domain}. thú", "annual_report.summary.percentile.we_wont_tell_bernie": "Ní inseoidh muid do Bernie.", + "annual_report.summary.share_elsewhere": "Roinn in áit eile", "annual_report.summary.share_message": "Fuair ​​mé an t-archetíopa {archetype}!", - "annual_report.summary.thanks": "Go raibh maith agat as a bheith mar chuid de Mastodon!", + "annual_report.summary.share_on_mastodon": "Comhroinn ar Mastodon", "attachments_list.unprocessed": "(neamhphróiseáilte)", "audio.hide": "Cuir fuaim i bhfolach", "block_modal.remote_users_caveat": "Iarrfaimid ar an bhfreastalaí {domain} meas a bheith agat ar do chinneadh. Mar sin féin, ní ráthaítear comhlíonadh toisc go bhféadfadh roinnt freastalaithe bloic a láimhseáil ar bhealach difriúil. Seans go mbeidh postálacha poiblí fós le feiceáil ag úsáideoirí nach bhfuil logáilte isteach.", @@ -248,7 +270,7 @@ "confirmations.follow_to_list.title": "Lean an t-úsáideoir?", "confirmations.logout.confirm": "Logáil amach", "confirmations.logout.message": "An bhfuil tú cinnte gur mhaith leat logáil amach?", - "confirmations.logout.title": "Logáil Amach?", + "confirmations.logout.title": "Logáil amach?", "confirmations.missing_alt_text.confirm": "Cuir téacs alt leis", "confirmations.missing_alt_text.message": "Tá meáin gan alt téacs i do phostáil. Má chuirtear tuairiscí leis, cabhraíonn sé seo leat d’inneachar a rochtain do níos mó daoine.", "confirmations.missing_alt_text.secondary": "Post ar aon nós", @@ -419,6 +441,8 @@ "follow_suggestions.who_to_follow": "Cé le leanúint", "followed_tags": "Hashtags le leanúint", "footer.about": "Maidir le", + "footer.about_mastodon": "Maidir le Mastodon", + "footer.about_server": "Maidir le {domain}", "footer.about_this_server": "Maidir", "footer.directory": "Eolaire próifílí", "footer.get_app": "Faigh an aip", @@ -456,7 +480,7 @@ "hints.profiles.see_more_followers": "Féach ar a thuilleadh leantóirí ar {domain}", "hints.profiles.see_more_follows": "Féach tuilleadh seo a leanas ar {domain}", "hints.profiles.see_more_posts": "Féach ar a thuilleadh postálacha ar {domain}", - "home.column_settings.show_quotes": "Taispeáin Sleachta", + "home.column_settings.show_quotes": "Taispeáin sleachta", "home.column_settings.show_reblogs": "Taispeáin moltaí", "home.column_settings.show_replies": "Taispeán freagraí", "home.hide_announcements": "Cuir fógraí i bhfolach", @@ -686,7 +710,7 @@ "notifications.column_settings.mention": "Tráchtanna:", "notifications.column_settings.poll": "Torthaí suirbhéanna:", "notifications.column_settings.push": "Brúfhógraí", - "notifications.column_settings.quote": "Luachain:", + "notifications.column_settings.quote": "Sleachta:", "notifications.column_settings.reblog": "Moltaí:", "notifications.column_settings.show": "Taispeáin i gcolún", "notifications.column_settings.sound": "Seinn an fhuaim", @@ -763,14 +787,14 @@ "privacy.public.long": "Duine ar bith ar agus amach Mastodon", "privacy.public.short": "Poiblí", "privacy.quote.anyone": "{visibility}, is féidir le duine ar bith lua", - "privacy.quote.disabled": "{visibility}, comharthaí athfhriotail díchumasaithe", - "privacy.quote.limited": "{visibility}, luachana teoranta", + "privacy.quote.disabled": "{visibility}, sleachta díchumasaithe", + "privacy.quote.limited": "{visibility}, sleachta teoranta", "privacy.unlisted.additional": "Iompraíonn sé seo díreach mar a bheadh ​​poiblí, ach amháin ní bheidh an postáil le feiceáil i bhfothaí beo nó i hashtags, in iniúchadh nó i gcuardach Mastodon, fiú má tá tú liostáilte ar fud an chuntais.", "privacy.unlisted.long": "I bhfolach ó thorthaí cuardaigh Mastodon, treochtaí, agus amlínte poiblí", "privacy.unlisted.short": "Poiblí ciúin", "privacy_policy.last_updated": "Nuashonraithe {date}", "privacy_policy.title": "Polasaí príobháideachais", - "quote_error.edit": "Ní féidir Sleachta a chur leis agus post á chur in eagar.", + "quote_error.edit": "Ní féidir sleachta a chur leis agus post á chur in eagar.", "quote_error.poll": "Ní cheadaítear lua le pobalbhreitheanna.", "quote_error.private_mentions": "Ní cheadaítear lua le tagairtí díreacha.", "quote_error.quote": "Ní cheadaítear ach luachan amháin ag an am.", @@ -885,7 +909,7 @@ "status.admin_account": "Oscail comhéadan modhnóireachta do @{name}", "status.admin_domain": "Oscail comhéadan modhnóireachta le haghaidh {domain}", "status.admin_status": "Oscail an postáil seo sa chomhéadan modhnóireachta", - "status.all_disabled": "Tá borradh agus luachana díchumasaithe", + "status.all_disabled": "Tá treisiúcháin agus sleachta díchumasaithe", "status.block": "Bac @{name}", "status.bookmark": "Leabharmharcanna", "status.cancel_reblog_private": "Dímhol", @@ -945,7 +969,7 @@ "status.quotes.empty": "Níl an post seo luaite ag aon duine go fóill. Nuair a dhéanann duine é, taispeánfar anseo é.", "status.quotes.local_other_disclaimer": "Ní thaispeánfar sleachta ar dhiúltaigh an t-údar dóibh.", "status.quotes.remote_other_disclaimer": "Níl ráthaíocht ann go dtaispeánfar anseo ach sleachta ó {domain}. Ní thaispeánfar sleachta ar dhiúltaigh an t-údar dóibh.", - "status.quotes_count": "{count, plural,\n one {{counter} athfhriotal}\n two {{counter} athfhriotail}\n few {{counter} athfhriotail}\n many {{counter} athfhriotal}\n other {{counter} athfhriotail}\n}", + "status.quotes_count": "{count, plural, one {{counter} sleacht} two {{counter} sleachta} few {{counter} sleachta} many {{counter} sleachta} other {{counter} sleachta}}", "status.read_more": "Léan a thuilleadh", "status.reblog": "Treisiú", "status.reblog_or_quote": "Borradh nó luachan", @@ -1001,7 +1025,7 @@ "upload_button.label": "Cuir íomhánna, físeán nó comhad fuaime leis", "upload_error.limit": "Sáraíodh an teorainn uaslódála comhaid.", "upload_error.poll": "Ní cheadaítear uaslódáil comhad le pobalbhreith.", - "upload_error.quote": "Ní cheadaítear uaslódáil comhaid le comharthaí athfhriotail.", + "upload_error.quote": "Ní cheadaítear uaslódáil comhaid le sleachta.", "upload_form.drag_and_drop.instructions": "Chun ceangaltán meán a phiocadh suas, brúigh spás nó cuir isteach. Agus tú ag tarraingt, bain úsáid as na heochracha saigheada chun an ceangaltán meán a bhogadh i dtreo ar bith. Brúigh spás nó cuir isteach arís chun an ceangaltán meán a scaoileadh ina phost nua, nó brúigh éalú chun cealú.", "upload_form.drag_and_drop.on_drag_cancel": "Cuireadh an tarraingt ar ceal. Scaoileadh ceangaltán meán {item}.", "upload_form.drag_and_drop.on_drag_end": "Scaoileadh ceangaltán meán {item}.", @@ -1027,11 +1051,11 @@ "video.volume_up": "Toirt suas", "visibility_modal.button_title": "Socraigh infheictheacht", "visibility_modal.direct_quote_warning.text": "Má shábhálann tú na socruithe reatha, déanfar an luachan leabaithe a thiontú ina nasc.", - "visibility_modal.direct_quote_warning.title": "Ní féidir luachana a leabú i luanna príobháideacha", + "visibility_modal.direct_quote_warning.title": "Ní féidir sleachta a leabú i dtráchtanna príobháideacha", "visibility_modal.header": "Infheictheacht agus idirghníomhaíocht", "visibility_modal.helper.direct_quoting": "Ní féidir le daoine eile tráchtanna príobháideacha a scríobhadh ar Mastodon a lua.", "visibility_modal.helper.privacy_editing": "Ní féidir infheictheacht a athrú tar éis post a fhoilsiú.", - "visibility_modal.helper.privacy_private_self_quote": "Ní féidir féin-luachanna ó phoist phríobháideacha a chur ar fáil don phobal.", + "visibility_modal.helper.privacy_private_self_quote": "Ní féidir féin-sleachta ó phoist phríobháideacha a chur ar fáil don phobal.", "visibility_modal.helper.private_quoting": "Ní féidir le daoine eile poist atá scríofa ar Mastodon agus atá dírithe ar leanúna amháin a lua.", "visibility_modal.helper.unlisted_quoting": "Nuair a luann daoine thú, beidh a bpost i bhfolach ó amlínte treochta freisin.", "visibility_modal.instructions": "Rialaigh cé a fhéadfaidh idirghníomhú leis an bpost seo. Is féidir leat socruithe a chur i bhfeidhm ar gach post amach anseo trí nascleanúint a dhéanamh chuig Sainroghanna > Réamhshocruithe Postála.", diff --git a/app/javascript/mastodon/locales/gd.json b/app/javascript/mastodon/locales/gd.json index e24a7d4608fdc4..16ce197f3f738c 100644 --- a/app/javascript/mastodon/locales/gd.json +++ b/app/javascript/mastodon/locales/gd.json @@ -113,25 +113,11 @@ "alt_text_modal.describe_for_people_with_visual_impairments": "Mìnich seo dhan fheadhainn air a bheil cion-lèirsinne…", "alt_text_modal.done": "Deiseil", "announcement.announcement": "Brath-fios", - "annual_report.summary.archetype.booster": "Brosnaiche", - "annual_report.summary.archetype.lurker": "Eala-bhalbh", - "annual_report.summary.archetype.oracle": "Coinneach Odhar", - "annual_report.summary.archetype.pollster": "Cunntair nam beachd", - "annual_report.summary.archetype.replier": "Ceatharnach nam freagairt", - "annual_report.summary.followers.followers": "luchd-leantainn", - "annual_report.summary.followers.total": "{count} gu h-iomlan", - "annual_report.summary.here_it_is": "Seo mar a chaidh {year} leat:", - "annual_report.summary.highlighted_post.by_favourites": "am post as annsa", - "annual_report.summary.highlighted_post.by_reblogs": "am post air a bhrosnachadh as trice", - "annual_report.summary.highlighted_post.by_replies": "am post dhan deach fhreagairt as trice", - "annual_report.summary.highlighted_post.possessive": "Aig {name},", "annual_report.summary.most_used_app.most_used_app": "an aplacaid a chaidh a cleachdadh as trice", "annual_report.summary.most_used_hashtag.most_used_hashtag": "an taga hais a chaidh a cleachdadh as trice", - "annual_report.summary.most_used_hashtag.none": "Chan eil gin", "annual_report.summary.new_posts.new_posts": "postaichean ùra", "annual_report.summary.percentile.text": "Tha thu am measgdhen luchd-cleachdaidh as cliùitiche air {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "Ainmeil ’nad latha ’s ’nad linn.", - "annual_report.summary.thanks": "Mòran taing airson conaltradh air Mastodon!", "attachments_list.unprocessed": "(gun phròiseasadh)", "audio.hide": "Falaich an fhuaim", "block_modal.remote_users_caveat": "Iarraidh sinn air an fhrithealaiche {domain} gun gèill iad ri do cho-dhùnadh. Gidheadh, chan eil barantas gun gèill iad on a làimhsicheas cuid a fhrithealaichean bacaidhean air dòigh eadar-dhealaichte. Dh’fhaoidte gum faic daoine gun chlàradh a-steach na postaichean poblach agad fhathast.", diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json index dad94113a9323a..d707ba57100b2b 100644 --- a/app/javascript/mastodon/locales/gl.json +++ b/app/javascript/mastodon/locales/gl.json @@ -114,29 +114,51 @@ "alt_text_modal.done": "Feito", "announcement.announcement": "Anuncio", "annual_report.announcement.action_build": "Crear o meu Wrapstodon", + "annual_report.announcement.action_dismiss": "Non, grazas", "annual_report.announcement.action_view": "Ver o meu Wrapstodon", "annual_report.announcement.description": "Olla o que andiveches facendo en Mastodon durante o último ano.", "annual_report.announcement.title": "Chegou o Wrapstodon de {year}", - "annual_report.summary.archetype.booster": "O Telexornal", - "annual_report.summary.archetype.lurker": "Volleur", - "annual_report.summary.archetype.oracle": "Sabichón", - "annual_report.summary.archetype.pollster": "I.G.E.", - "annual_report.summary.archetype.replier": "Lareteire", - "annual_report.summary.followers.followers": "seguidoras", - "annual_report.summary.followers.total": "{count} en total", - "annual_report.summary.here_it_is": "Este é o resumo do teu {year}:", - "annual_report.summary.highlighted_post.by_favourites": "a publicación mais favorecida", - "annual_report.summary.highlighted_post.by_reblogs": "a publicación con mais promocións", - "annual_report.summary.highlighted_post.by_replies": "a publicación con mais respostas", - "annual_report.summary.highlighted_post.possessive": "de {name}", + "annual_report.nav_item.badge": "Novidade", + "annual_report.shared_page.donate": "Doar", + "annual_report.shared_page.footer": "Creado con {heart} polo equipo de Mastodon", + "annual_report.shared_page.footer_server_info": "{username} utiliza {domain}, unha das moitas comunidades de Mastodon.", + "annual_report.summary.archetype.booster.desc_public": "{name} á caza de publicacións para promover, dando relevancia a outras creadoras.", + "annual_report.summary.archetype.booster.desc_self": "Á caza de publicacións para promover, dando relevancia a outras creadoras.", + "annual_report.summary.archetype.booster.name": "Arqueire", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.desc_public": "Sabemos que {name} andivo por aí, nalgures, desfrutando de Mastodon paseniño e ao seu xeito.", + "annual_report.summary.archetype.lurker.desc_self": "Sabemos que andiveches por aí, nalgures, desfrutando de Mastodon tranquilamente e ao teu xeito.", + "annual_report.summary.archetype.lurker.name": "Xubilade", + "annual_report.summary.archetype.oracle.desc_public": "{name} creou máis publicacións que respostas, mantendo Mastodon ao día e ollando ao futuro.", + "annual_report.summary.archetype.oracle.desc_self": "Creaches máis publicacións que respostas, mantendo Mastodon ao día e ollando o futuro.", + "annual_report.summary.archetype.oracle.name": "O Oráculo", + "annual_report.summary.archetype.pollster.desc_public": "{name} creou máis enquisas que publicacións normais, cultivando a curiosidade en Mastodon.", + "annual_report.summary.archetype.pollster.desc_self": "Creaches máis enquisas que publicacións normais, cultivando a curiosidade en Mastodon.", + "annual_report.summary.archetype.pollster.name": "O mar de dúbidas", + "annual_report.summary.archetype.replier.desc_public": "{name} respondeu con frecuencia a outras persoas, polinizando Mastodon con novas conversas.", + "annual_report.summary.archetype.replier.desc_self": "Respondeches con frecuencia a outras persoas, polinizando Mastodon con novas conversas.", + "annual_report.summary.archetype.replier.name": "A Bolboreta", + "annual_report.summary.archetype.reveal": "Mostrar o meu arquetipo", + "annual_report.summary.archetype.reveal_description": "Grazas por ser parte de Mastodon! É hora de coñecer o teu arquetipo de usuaria no {year}.", + "annual_report.summary.archetype.title_public": "Arquetipo de {name}", + "annual_report.summary.archetype.title_self": "O teu arquetipo", + "annual_report.summary.close": "Fechar", + "annual_report.summary.copy_link": "Copiar ligazón", + "annual_report.summary.followers.new_followers": "{count, plural, one {nova seguidora} other {nevas seguidoras}}", + "annual_report.summary.highlighted_post.boost_count": "Esta publicación promoveuse {count, plural, one {unha vez} other {# veces}}.", + "annual_report.summary.highlighted_post.favourite_count": "Esta publicación favoreceuse {count, plural, one {unha vez} other {# veces}}.", + "annual_report.summary.highlighted_post.reply_count": "Esta publicación tivo {count, plural, one {unha resposta} other {# respostas}}.", + "annual_report.summary.highlighted_post.title": "Publicación máis popular", "annual_report.summary.most_used_app.most_used_app": "app que mais usaches", "annual_report.summary.most_used_hashtag.most_used_hashtag": "o cancelo mais utilizado", - "annual_report.summary.most_used_hashtag.none": "Nada", + "annual_report.summary.most_used_hashtag.used_count": "Incluíches este cancelo en {count, plural, one {1 publicación} other {# publicacións}}.", + "annual_report.summary.most_used_hashtag.used_count_public": "{name} incluíu este cancelo en {count, plural, one {1 publicación} other {# publicacións}}.", "annual_report.summary.new_posts.new_posts": "novas publicacións", "annual_report.summary.percentile.text": "Sitúante no top das usuarias de {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "Moito tes que contarnos!", + "annual_report.summary.share_elsewhere": "Compárteo onde queiras", "annual_report.summary.share_message": "Resulta que son… {archetype}!", - "annual_report.summary.thanks": "Grazas por ser parte de Mastodon!", + "annual_report.summary.share_on_mastodon": "Compartir en Mastodon", "attachments_list.unprocessed": "(sen procesar)", "audio.hide": "Agochar audio", "block_modal.remote_users_caveat": "Ímoslle pedir ao servidor {domain} que respecte a túa decisión. Emporiso, non hai garantía de que atenda a petición xa que os servidores xestionan os bloqueos de formas diferentes. As publicacións públicas poderían aínda ser visibles para usuarias que non iniciaron sesión.", @@ -419,6 +441,8 @@ "follow_suggestions.who_to_follow": "A quen seguir", "followed_tags": "Cancelos seguidos", "footer.about": "Sobre", + "footer.about_mastodon": "Sobre Mastodon", + "footer.about_server": "Sobre {domain}", "footer.about_this_server": "Sobre", "footer.directory": "Directorio de perfís", "footer.get_app": "Descarga a app", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index ea0bc4b44fa9cd..3cee7adc5e12e7 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -114,29 +114,51 @@ "alt_text_modal.done": "סיום", "announcement.announcement": "הכרזה", "annual_report.announcement.action_build": "בנה לי את הסיכומודון שלי", + "annual_report.announcement.action_dismiss": "לא תודה", "annual_report.announcement.action_view": "לצפייה בסיכומודון שלי", "annual_report.announcement.description": "ללמוד עוד על דבפוסי השימוש שלך במסטודון לאורך השנה החולפת.", "annual_report.announcement.title": "סיכומודון {year} הגיע", - "annual_report.summary.archetype.booster": "ההד-וניסט(ית)", - "annual_report.summary.archetype.lurker": "השורץ.ת השקט.ה", - "annual_report.summary.archetype.oracle": "כבוד הרב.ה", - "annual_report.summary.archetype.pollster": "הסקרן.ית", - "annual_report.summary.archetype.replier": "הפרפר.ית החברתי.ת", - "annual_report.summary.followers.followers": "עוקבים", - "annual_report.summary.followers.total": "{count} בסך הכל", - "annual_report.summary.here_it_is": "והנה סיכום {year} שלך:", - "annual_report.summary.highlighted_post.by_favourites": "התות הכי מחובב", - "annual_report.summary.highlighted_post.by_reblogs": "התות הכי מהודהד", - "annual_report.summary.highlighted_post.by_replies": "התות עם מספר התשובות הגבוה ביותר", - "annual_report.summary.highlighted_post.possessive": "של {name}", + "annual_report.nav_item.badge": "חדש", + "annual_report.shared_page.donate": "לתרומה", + "annual_report.shared_page.footer": "נוצר עם כל ה-{heart} על ידי צוות מסטודון", + "annual_report.shared_page.footer_server_info": "{username} משתמש.ת ב־{domain}, שרת אחד מבין קהילות רבות שבנויות על מסטודון.", + "annual_report.summary.archetype.booster.desc_public": "{name} צדו הודעות מעניינות להדהד, והגבירו קולותיהם של יוצרים אחרים בדיוק של חתול המזנק על הטרף.", + "annual_report.summary.archetype.booster.desc_self": "צדת הודעות מעניינות להדהד, והגברת קולותיהם של יוצרים אחרים בדיוק של חתול המזנק על הטרף.", + "annual_report.summary.archetype.booster.name": "החתול הצייד", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.desc_public": "למיטב ידיעתנו {name} היו שם, איפשהוא, נהנים ממסטודון בדרכם השקטה.", + "annual_report.summary.archetype.lurker.desc_self": "למיטב ידיעתנו היית שם, איפשהוא, נהנית ממסטודון בדרכך השקטה.", + "annual_report.summary.archetype.lurker.name": "הסטואי.ת", + "annual_report.summary.archetype.oracle.desc_public": "{name} יצרו יותר הודעות מאשר תגובות, ושמרו על מסטודון רעננה ועם פנים לעתיד.", + "annual_report.summary.archetype.oracle.desc_self": "יצרת יותר הודעות מאשר תגובות, ושמרת על מסטודון רעננה ועם פנים לעתיד.", + "annual_report.summary.archetype.oracle.name": "כבוד הרב.ה", + "annual_report.summary.archetype.pollster.desc_public": "{name} יצרו יותר סקרים מאשר כל סוג הודעה אחר, וכך הרבו סקרנות במסטודון.", + "annual_report.summary.archetype.pollster.desc_self": "יצרת יותר סקרים מאשר כל סוג הודעה אחר, וכך הרבת סקרנות במסטודון.", + "annual_report.summary.archetype.pollster.name": "התוהה", + "annual_report.summary.archetype.replier.desc_public": "{name} ענו תכופות לאחרים, והפרו את מסטודון בדיונים חדשים.", + "annual_report.summary.archetype.replier.desc_self": "ענית תכופות לאחרים, והפרית את מסטודון בדיונים חדשים.", + "annual_report.summary.archetype.replier.name": "הפרפר", + "annual_report.summary.archetype.reveal": "גלו את הארכיטיפ שלכם", + "annual_report.summary.archetype.reveal_description": "תודה שאתם חלק ממסטודון! הגיע הזמן לגלות מה הארכיטיפ שגילמתן בשנת {year}.", + "annual_report.summary.archetype.title_public": "הארכיטיפ של {name}", + "annual_report.summary.archetype.title_self": "הארכיטיפ שלך", + "annual_report.summary.close": "סגירה", + "annual_report.summary.copy_link": "העתק קישור", + "annual_report.summary.followers.new_followers": "{count, plural,one {עוקב חדש} other {עוקבים חדשים}}", + "annual_report.summary.highlighted_post.boost_count": "הודעה זו הודהדה {count, plural,one {פעם אחת}other {# פעמים}}.", + "annual_report.summary.highlighted_post.favourite_count": "הודעה זו חובבה {count, plural,one {פעם אחת}other {# פעמים}}.", + "annual_report.summary.highlighted_post.reply_count": "הודעה זו נענתה על ידי {count, plural,one {תגובה אחת}other {# תגובות}}.", + "annual_report.summary.highlighted_post.title": "ההודעה הפופולרית ביותר", "annual_report.summary.most_used_app.most_used_app": "היישומון שהכי בשימוש", "annual_report.summary.most_used_hashtag.most_used_hashtag": "התג בשימוש הרב ביותר", - "annual_report.summary.most_used_hashtag.none": "אף אחד", + "annual_report.summary.most_used_hashtag.used_count": "כללת את התגית הזו {count, plural,one {בהודעה אחת}other {ב-# הודעות}}.", + "annual_report.summary.most_used_hashtag.used_count_public": "{name} כללו את התגית {count, plural,one {בהודעה אחת}other {ב־# הודעות}}.", "annual_report.summary.new_posts.new_posts": "הודעות חדשות", "annual_report.summary.percentile.text": "ממקם אותך באחוזון של משמשי {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "לא נגלה לברני.", + "annual_report.summary.share_elsewhere": "שיתוף במקום אחר", "annual_report.summary.share_message": "זוהיתי כדוגמא לטיפוס {archetype}!", - "annual_report.summary.thanks": "תודה על היותך חלק ממסטודון!", + "annual_report.summary.share_on_mastodon": "לשתף במסטודון", "attachments_list.unprocessed": "(לא מעובד)", "audio.hide": "השתק", "block_modal.remote_users_caveat": "אנו נבקש מהשרת {domain} לכבד את החלטתך. עם זאת, ציות למוסכמות איננו מובטח כיוון ששרתים מסויימים עשויים לטפל בחסימות בצורה אחרת. הודעות פומביות עדיין יהיו גלויות לעיני משתמשים שאינם מחוברים.", @@ -419,6 +441,8 @@ "follow_suggestions.who_to_follow": "אחרי מי לעקוב", "followed_tags": "התגיות שהחשבון שלך עוקב אחריהן", "footer.about": "אודות", + "footer.about_mastodon": "אודות מסטודון", + "footer.about_server": "‮אודות ‭{domain}", "footer.about_this_server": "אודות", "footer.directory": "ספריית פרופילים", "footer.get_app": "להתקנת היישומון", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index c62a332a96d70b..c21b6016f08e46 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -114,28 +114,51 @@ "alt_text_modal.done": "Kész", "announcement.announcement": "Közlemény", "annual_report.announcement.action_build": "Saját Wrapstodon összeállítása", + "annual_report.announcement.action_dismiss": "Nem, köszönöm", "annual_report.announcement.action_view": "Saját Wrapstodon megtekintése", "annual_report.announcement.description": "Fedezz fel többet a Mastodonon az elmúlt évben végzett tevékenységeidről.", "annual_report.announcement.title": "A Wrapstodon {year} megérkezett", - "annual_report.summary.archetype.booster": "A cool-vadász", - "annual_report.summary.archetype.lurker": "A settenkedő", - "annual_report.summary.archetype.oracle": "Az orákulum", - "annual_report.summary.archetype.pollster": "A közvélemény-kutató", - "annual_report.summary.archetype.replier": "A társasági pillangó", - "annual_report.summary.followers.followers": "követő", - "annual_report.summary.followers.total": "{count} összesen", - "annual_report.summary.here_it_is": "Itt a {year}. év értékelése:", - "annual_report.summary.highlighted_post.by_favourites": "legkedvencebb bejegyzés", - "annual_report.summary.highlighted_post.by_reblogs": "legtöbbet megtolt bejegyzés", - "annual_report.summary.highlighted_post.by_replies": "bejegyzés a legtöbb válasszal", - "annual_report.summary.highlighted_post.possessive": "{name} fióktól", + "annual_report.nav_item.badge": "Új", + "annual_report.shared_page.donate": "Adományozás", + "annual_report.shared_page.footer": "A Mastodon által {heart}-tel előállítva", + "annual_report.shared_page.footer_server_info": "{username} a {domain} domaint használja, amely a Mastodon által működtetett számos közösség egyike.", + "annual_report.summary.archetype.booster.desc_public": "{name} megtolandó bejegyzésekre vadászott, tökéletes célzással felerősítve mások üzeneteit.", + "annual_report.summary.archetype.booster.desc_self": "Megtolandó bejegyzésekre vadásztál, tökéletes célzással felerősítve mások üzeneteit.", + "annual_report.summary.archetype.booster.name": "Az íjász", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.desc_public": "Tudjuk, hogy {name} ott volt, valahol, a saját csendes módján élvezve a Mastodont.", + "annual_report.summary.archetype.lurker.desc_self": "Tudjuk, hogy ott voltál, valahol, a saját csendes módodon élvezve a Mastodont.", + "annual_report.summary.archetype.lurker.name": "A sztoikus", + "annual_report.summary.archetype.oracle.desc_public": "{name} több bejegyzést hozott létre, mint választ, így a Mastodon friss és jövőbe tekintő marad.", + "annual_report.summary.archetype.oracle.desc_self": "Több bejegyzést hoztál létre, mint választ, így a Mastodon friss és jövőbe tekintő marad.", + "annual_report.summary.archetype.oracle.name": "Az orákulum", + "annual_report.summary.archetype.pollster.desc_public": "{name} több szavazást hozott létre, mint bármilyen más bejegyzést, ezzel támogatva a kíváncsiságot a Mastodonon.", + "annual_report.summary.archetype.pollster.desc_self": "Több szavazást hoztál létre, mint bármilyen más bejegyzést, ezzel támogatva a kíváncsiságot a Mastodonon.", + "annual_report.summary.archetype.pollster.name": "A csodálkozó", + "annual_report.summary.archetype.replier.desc_public": "{name} gyakran válaszolt más emberek bejegyzéseire, új témákat porozva be a Mastodonon.", + "annual_report.summary.archetype.replier.desc_self": "Gyakran válaszoltál más emberek bejegyzéseire, új témákat porozva be a Mastodonon.", + "annual_report.summary.archetype.replier.name": "A pillangó", + "annual_report.summary.archetype.reveal": "Saját archetípus felfedése", + "annual_report.summary.archetype.reveal_description": "Köszönjük, hogy a Mastodon része vagy! Tudj meg többet az archetípusodról {year} évében.", + "annual_report.summary.archetype.title_public": "{name} archetípusa", + "annual_report.summary.archetype.title_self": "Saját archetípus", + "annual_report.summary.close": "Bezárás", + "annual_report.summary.copy_link": "Hivatkozás másolása", + "annual_report.summary.followers.new_followers": "{count, plural, one {új követő} other {új követők}}", + "annual_report.summary.highlighted_post.boost_count": "Ez a bejegyzés {count, plural, one {egyszer} other {# alkalommal}} volt megtolva.", + "annual_report.summary.highlighted_post.favourite_count": "Ez a bejegyzés {count, plural, one {egyszer} other {# alkalommal}} volt kedvencnek jelölve.", + "annual_report.summary.highlighted_post.reply_count": "Ez a bejegyzés {count, plural, one {egy választ} other {# választ}} kapott.", + "annual_report.summary.highlighted_post.title": "Legnépszerűbb bejegyzés", "annual_report.summary.most_used_app.most_used_app": "legtöbbet használt app", "annual_report.summary.most_used_hashtag.most_used_hashtag": "legtöbbet használt hashtag", - "annual_report.summary.most_used_hashtag.none": "Nincs", + "annual_report.summary.most_used_hashtag.used_count": "Ez a hashtag {count, plural, one {egy bejegyzésedben} other {# bejegyzésedben}} szerepel.", + "annual_report.summary.most_used_hashtag.used_count_public": "{name} {count, plural, one {egy bejegyzésében} other {# bejegyzésében}} használta ezt a hashtaget.", "annual_report.summary.new_posts.new_posts": "új bejegyzés", "annual_report.summary.percentile.text": "Ezzel a csúcs{domain} felhasználó között vagy.", "annual_report.summary.percentile.we_wont_tell_bernie": "Nem mondjuk el Bernie-nek.", - "annual_report.summary.thanks": "Kösz, hogy a Mastodon része vagy!", + "annual_report.summary.share_elsewhere": "Megosztás máshol", + "annual_report.summary.share_message": "{archetype} lett az archetípusom!", + "annual_report.summary.share_on_mastodon": "Megosztás a Mastodonon", "attachments_list.unprocessed": "(feldolgozatlan)", "audio.hide": "Hang elrejtése", "block_modal.remote_users_caveat": "Arra kérjük a {domain} kiszolgálót, hogy tartsa tiszteletben a döntésedet. Ugyanakkor az együttműködés nem garantált, mivel néhány kiszolgáló másképp kezelheti a letiltásokat. A nyilvános bejegyzések a be nem jelentkezett felhasználók számára továbbra is látszódhatnak.", @@ -418,6 +441,8 @@ "follow_suggestions.who_to_follow": "Kit érdemes követni", "followed_tags": "Követett hashtagek", "footer.about": "Névjegy", + "footer.about_mastodon": "A Mastodonról", + "footer.about_server": "A {domain} domainről", "footer.about_this_server": "Névjegy", "footer.directory": "Profiltár", "footer.get_app": "Alkalmazás beszerzése", diff --git a/app/javascript/mastodon/locales/ia.json b/app/javascript/mastodon/locales/ia.json index 4202a855c81b8b..a32a39617bd6f5 100644 --- a/app/javascript/mastodon/locales/ia.json +++ b/app/javascript/mastodon/locales/ia.json @@ -113,25 +113,11 @@ "alt_text_modal.describe_for_people_with_visual_impairments": "Describe isto pro personas con impedimentos visual…", "alt_text_modal.done": "Preste", "announcement.announcement": "Annuncio", - "annual_report.summary.archetype.booster": "Le impulsator", - "annual_report.summary.archetype.lurker": "Le lector", - "annual_report.summary.archetype.oracle": "Le oraculo", - "annual_report.summary.archetype.pollster": "Le sondagista", - "annual_report.summary.archetype.replier": "Le responditor", - "annual_report.summary.followers.followers": "sequitores", - "annual_report.summary.followers.total": "{count} in total", - "annual_report.summary.here_it_is": "Ecce tu summario de {year}:", - "annual_report.summary.highlighted_post.by_favourites": "message le plus favorite", - "annual_report.summary.highlighted_post.by_reblogs": "message le plus impulsate", - "annual_report.summary.highlighted_post.by_replies": "message le plus respondite", - "annual_report.summary.highlighted_post.possessive": "{name}, ecce tu…", "annual_report.summary.most_used_app.most_used_app": "application le plus usate", "annual_report.summary.most_used_hashtag.most_used_hashtag": "hashtag le plus usate", - "annual_report.summary.most_used_hashtag.none": "Necun", "annual_report.summary.new_posts.new_posts": "nove messages", "annual_report.summary.percentile.text": "Isto te pone in le primeusatores de {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "Tu es un primo inter pares.", - "annual_report.summary.thanks": "Gratias pro facer parte de Mastodon!", "attachments_list.unprocessed": "(non processate)", "audio.hide": "Celar audio", "block_modal.remote_users_caveat": "Nos demandera al servitor {domain} de respectar tu decision. Nonobstante, le conformitate non es garantite perque alcun servitores pote tractar le blocadas de maniera differente. Le messages public pote esser totevia visibile pro le usatores non authenticate.", diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json index 8ee26722501799..adf604a5d5d124 100644 --- a/app/javascript/mastodon/locales/io.json +++ b/app/javascript/mastodon/locales/io.json @@ -90,25 +90,11 @@ "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": "To pozas vu sur la supro di uzanti di {domain}.", "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.", diff --git a/app/javascript/mastodon/locales/is.json b/app/javascript/mastodon/locales/is.json index 5462f0c0ba91b5..a9b8011b4aee01 100644 --- a/app/javascript/mastodon/locales/is.json +++ b/app/javascript/mastodon/locales/is.json @@ -113,30 +113,52 @@ "alt_text_modal.describe_for_people_with_visual_impairments": "Lýstu þessu fyrir fólk með skerta sjón…", "alt_text_modal.done": "Lokið", "announcement.announcement": "Auglýsing", - "annual_report.announcement.action_build": "Byggja Wrapstodon fyrir mig", - "annual_report.announcement.action_view": "Skoða minn Wrapstodon", + "annual_report.announcement.action_build": "Búa til ársuppgjör fyrir mig", + "annual_report.announcement.action_dismiss": "Nei takk", + "annual_report.announcement.action_view": "Skoða ársuppgjörið mitt", "annual_report.announcement.description": "Skoðaðu meira um virkni þína á Mastodon á síðastliðnu ári.", - "annual_report.announcement.title": "Wrapstodon {year} er komið", - "annual_report.summary.archetype.booster": "Svali gaurinn", - "annual_report.summary.archetype.lurker": "Lurkurinn", - "annual_report.summary.archetype.oracle": "Völvan", - "annual_report.summary.archetype.pollster": "Kannanafíkillinn", - "annual_report.summary.archetype.replier": "Félagsveran", - "annual_report.summary.followers.followers": "fylgjendur", - "annual_report.summary.followers.total": "{count} alls", - "annual_report.summary.here_it_is": "Hér er yfirlitið þitt fyrir {year}:", - "annual_report.summary.highlighted_post.by_favourites": "færsla sett oftast í eftirlæti", - "annual_report.summary.highlighted_post.by_reblogs": "færsla oftast endurbirt", - "annual_report.summary.highlighted_post.by_replies": "færsla með flestum svörum", - "annual_report.summary.highlighted_post.possessive": "{name}", + "annual_report.announcement.title": "Ársuppgjörið {year} er komið", + "annual_report.nav_item.badge": "Nýtt", + "annual_report.shared_page.donate": "Styrkja", + "annual_report.shared_page.footer": "{heart}-kveðjur frá Mastodon-teyminu", + "annual_report.shared_page.footer_server_info": "{username} notar {domain}, einu af samfélögunum sem Mastodon drífur.", + "annual_report.summary.archetype.booster.desc_public": "{name} var á höttunum eftir færslum til að endurbirta og hitti þannig í mark með að magna upp það sem aðrir voru að gera.", + "annual_report.summary.archetype.booster.desc_self": "Þú varst á höttunum eftir færslum til að endurbirta og hittir þannig í mark með að magna upp það sem aðrir voru að gera.", + "annual_report.summary.archetype.booster.name": "Veiðmaðurinn", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.desc_public": "Við vitum að {name} var þarna úti, einhversstaðar, að njóta Mastodon á sínum eigin hljóðlátu forsendum.", + "annual_report.summary.archetype.lurker.desc_self": "Við vitum að þú varst þarna úti, einhversstaðar, að njóta Mastodon á þínum eigin hljóðlátu forsendum.", + "annual_report.summary.archetype.lurker.name": "Heimspekingurinn", + "annual_report.summary.archetype.oracle.desc_public": "{name} útbjó fleiri færslur en svör og hélt þannig Mastodon fersku og vísandi til framtíðar.", + "annual_report.summary.archetype.oracle.desc_self": "Þú útbjóst fleiri færslur en svör og hélst þannig Mastodon fersku og vísandi til framtíðar.", + "annual_report.summary.archetype.oracle.name": "Völvan", + "annual_report.summary.archetype.pollster.desc_public": "{name} útbjó fleiri kannanir en aðrar gerðir af færslum og ræktaði þannig forvitni á ökrum Mastodon.", + "annual_report.summary.archetype.pollster.desc_self": "Þú útbjóst fleiri kannanir en aðrar gerðir af færslum og ræktaðir þannig forvitni á ökrum Mastodon.", + "annual_report.summary.archetype.pollster.name": "Könnuðurinn", + "annual_report.summary.archetype.replier.desc_public": "{name} svaraði oft færslum annara og frjóvgaði Mastodon með nýjum samræðum.", + "annual_report.summary.archetype.replier.desc_self": "Þú svaraðir oft færslum annara og frjóvgaðir Mastodon með nýjum samræðum.", + "annual_report.summary.archetype.replier.name": "Fiðrildið", + "annual_report.summary.archetype.reveal": "Uppljóstra um erkitýpuna mína", + "annual_report.summary.archetype.reveal_description": "Takk fyrir að vera hluti af Mastodon! Nú er tíminn til að sjá hvaða erkitýpu þú líktist árið {year}.", + "annual_report.summary.archetype.title_public": "Erkitýpan fyrir {name}", + "annual_report.summary.archetype.title_self": "Erkitýpan þín", + "annual_report.summary.close": "Loka", + "annual_report.summary.copy_link": "Afrita tengil", + "annual_report.summary.followers.new_followers": "{count, plural, one {nýr fylgjandi} other {nýir fylgjendur}}", + "annual_report.summary.highlighted_post.boost_count": "Þessi færsla var endurbirt {count, plural, one {einu sinni} other {# sinnum}}.", + "annual_report.summary.highlighted_post.favourite_count": "Þessi færsla var sett í eftirlæti {count, plural, one {einu sinni} other {# sinnum}}.", + "annual_report.summary.highlighted_post.reply_count": "Þessi færsla fékk {count, plural, one {eitt svar} other {# svör}}.", + "annual_report.summary.highlighted_post.title": "Vinsælasta færslan", "annual_report.summary.most_used_app.most_used_app": "mest notaða forrit", "annual_report.summary.most_used_hashtag.most_used_hashtag": "mest notaða myllumerki", - "annual_report.summary.most_used_hashtag.none": "Ekkert", + "annual_report.summary.most_used_hashtag.used_count": "Þú hafðir þetta myllumerki með í {count, plural, one {einni færslu} other {# færslum}}.", + "annual_report.summary.most_used_hashtag.used_count_public": "{name} hafði þetta myllumerki með í {count, plural, one {einni færslu} other {# færslum}}.", "annual_report.summary.new_posts.new_posts": "nýjar færslur", "annual_report.summary.percentile.text": "Þetta setur þig á meðalof {domain} virkustu notendanna.", "annual_report.summary.percentile.we_wont_tell_bernie": "Við förum ekkert að raupa um þetta.", + "annual_report.summary.share_elsewhere": "Deila annarsstaðar", "annual_report.summary.share_message": "Ég er víst {archetype}-týpan!", - "annual_report.summary.thanks": "Takk fyrir að vera hluti af Mastodon-samfélaginu!", + "annual_report.summary.share_on_mastodon": "Deila á Mastodon", "attachments_list.unprocessed": "(óunnið)", "audio.hide": "Fela hljóð", "block_modal.remote_users_caveat": "Við munum biðja {domain} netþjóninn um að virða ákvörðun þína. Hitt er svo annað mál hvort hann fari eftir þessu, ekki er hægt að tryggja eftirfylgni því sumir netþjónar meðhöndla útilokanir á sinn hátt. Opinberar færslur gætu verið sýnilegar notendum sem ekki eru skráðir inn.", @@ -419,6 +441,8 @@ "follow_suggestions.who_to_follow": "Hverjum á að fylgjast með", "followed_tags": "Vöktuð myllumerki", "footer.about": "Nánari upplýsingar", + "footer.about_mastodon": "Um Mastodon", + "footer.about_server": "Um {domain}", "footer.about_this_server": "Nánari upplýsingar", "footer.directory": "Notandasniðamappa", "footer.get_app": "Ná í forritið", @@ -614,8 +638,8 @@ "notification.admin.report_statuses_other": "{name} kærði {target}", "notification.admin.sign_up": "{name} skráði sig", "notification.admin.sign_up.name_and_others": "{name} og {count, plural, one {# í viðbót hefur} other {# í viðbót hafa}} skráð sig", - "notification.annual_report.message": "{year} á #Wrapstodon bíður! Afhjúpaðu hvað bar hæst á árinu og minnistæðustu augnablikin á Mastodon!", - "notification.annual_report.view": "Skoða #Wrapstodon", + "notification.annual_report.message": "Ársuppgjörið {year} bíður á #Wrapstodon! Afhjúpaðu hvað bar hæst á árinu og minnistæðustu augnablikin á Mastodon!", + "notification.annual_report.view": "Skoða ársuppgjör á #Wrapstodon", "notification.favourite": "{name} setti færsluna þína í eftirlæti", "notification.favourite.name_and_others_with_link": "{name} og {count, plural, one {# í viðbót hefur} other {# í viðbót hafa}} sett færsluna þína í eftirlæti", "notification.favourite_pm": "{name} setti í eftirlæti færslu í einkaspjalli þar sem þú minntist á viðkomandi", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index d6a3d994ead6d2..3f509078594ffd 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -31,7 +31,7 @@ "account.edit_profile_short": "Modifica", "account.enable_notifications": "Avvisami quando @{name} pubblica un post", "account.endorse": "In evidenza sul profilo", - "account.familiar_followers_many": "Seguito da {name1}, {name2}, e {othersCount, plural, one {un altro che conosci} other {# altri che conosci}}", + "account.familiar_followers_many": "Seguito da {name1}, {name2}, e {othersCount, plural, one {un altro che conosci} other {altri # che conosci}}", "account.familiar_followers_one": "Seguito da {name1}", "account.familiar_followers_two": "Seguito da {name1} e {name2}", "account.featured": "In primo piano", @@ -114,29 +114,51 @@ "alt_text_modal.done": "Fatto", "announcement.announcement": "Annuncio", "annual_report.announcement.action_build": "Costruisci il mio Wrapstodon", + "annual_report.announcement.action_dismiss": "No, grazie", "annual_report.announcement.action_view": "Visualizza il mio Wrapstodon", "annual_report.announcement.description": "Scopri di più sul tuo coinvolgimento su Mastodon nell'ultimo anno.", "annual_report.announcement.title": "Wrapstodon {year} è arrivato", - "annual_report.summary.archetype.booster": "Cacciatore/trice di tendenze", - "annual_report.summary.archetype.lurker": "L'osservatore/trice", - "annual_report.summary.archetype.oracle": "L'oracolo", - "annual_report.summary.archetype.pollster": "Sondaggista", - "annual_report.summary.archetype.replier": "Utente socievole", - "annual_report.summary.followers.followers": "seguaci", - "annual_report.summary.followers.total": "{count} in totale", - "annual_report.summary.here_it_is": "Ecco il tuo {year} in sintesi:", - "annual_report.summary.highlighted_post.by_favourites": "il post più apprezzato", - "annual_report.summary.highlighted_post.by_reblogs": "il post più condiviso", - "annual_report.summary.highlighted_post.by_replies": "il post con più risposte", - "annual_report.summary.highlighted_post.possessive": "di {name}", + "annual_report.nav_item.badge": "Nuovo", + "annual_report.shared_page.donate": "Dona", + "annual_report.shared_page.footer": "Generato con {heart} dal team di Mastodon", + "annual_report.shared_page.footer_server_info": "{username} usa {domain}, una delle tante comunità basate su Mastodon.", + "annual_report.summary.archetype.booster.desc_public": "{name} è rimasto/a alla ricerca di post da condividere, amplificando il lavoro di altri creatori con precisione mirata.", + "annual_report.summary.archetype.booster.desc_self": "Sei rimasto/a alla ricerca di post da condividere, amplificando il lavoro di altri creatori con precisione mirata.", + "annual_report.summary.archetype.booster.name": "L'Arciere", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.desc_public": "Sappiamo che {name} era là fuori, da qualche parte, a godersi Mastodon in tutta tranquillità.", + "annual_report.summary.archetype.lurker.desc_self": "Sappiamo che eri là fuori, da qualche parte, a goderti Mastodon in tutta tranquillità.", + "annual_report.summary.archetype.lurker.name": "Lo Stoico", + "annual_report.summary.archetype.oracle.desc_public": "{name} ha creato più nuovi post che risposte, mantenendo Mastodon fresco e orientato al futuro.", + "annual_report.summary.archetype.oracle.desc_self": "Hai creato più nuovi post che risposte, mantenendo Mastodon fresco e orientato al futuro.", + "annual_report.summary.archetype.oracle.name": "L'Oracolo", + "annual_report.summary.archetype.pollster.desc_public": "{name} ha creato più sondaggi rispetto ad altri tipi di post, alimentando la curiosità su Mastodon.", + "annual_report.summary.archetype.pollster.desc_self": "Hai creato più sondaggi rispetto ad altri tipi di post, alimentando la curiosità su Mastodon.", + "annual_report.summary.archetype.pollster.name": "L'Esploratore", + "annual_report.summary.archetype.replier.desc_public": "{name} ha frequentemente risposto ai post di altre persone, alimentando Mastodon con nuove discussioni.", + "annual_report.summary.archetype.replier.desc_self": "Hai frequentemente risposto ai post di altre persone, alimentando Mastodon con nuove discussioni.", + "annual_report.summary.archetype.replier.name": "La Farfalla", + "annual_report.summary.archetype.reveal": "Rivela il mio archetipo", + "annual_report.summary.archetype.reveal_description": "Grazie per far parte di Mastodon! È il momento di scoprire quale archetipo hai incarnato nel {year}.", + "annual_report.summary.archetype.title_public": "Archetipo di {name}", + "annual_report.summary.archetype.title_self": "Il tuo archetipo", + "annual_report.summary.close": "Chiudi", + "annual_report.summary.copy_link": "Copia il сollegamento", + "annual_report.summary.followers.new_followers": "{count, plural, one {nuovo seguace} other {nuovi seguaci}}", + "annual_report.summary.highlighted_post.boost_count": "Questo post è stato condiviso {count, plural, one {1 volta} other {# volte}}.", + "annual_report.summary.highlighted_post.favourite_count": "Questo post è stato aggiunto ai preferiti {count, plural, one {1 volta} other {# volte}}.", + "annual_report.summary.highlighted_post.reply_count": "Questo post ha ricevuto {count, plural, one {1 risposta} other {# risposte}}.", + "annual_report.summary.highlighted_post.title": "Il post più popolare", "annual_report.summary.most_used_app.most_used_app": "l'app più utilizzata", "annual_report.summary.most_used_hashtag.most_used_hashtag": "l'hashtag più usato", - "annual_report.summary.most_used_hashtag.none": "Nessuno", + "annual_report.summary.most_used_hashtag.used_count": "Hai incluso questo hashtag in {count, plural, one {1 post} other {# post}}.", + "annual_report.summary.most_used_hashtag.used_count_public": "{name} ha incluso questo hashtag in {count, plural, one {1 post} other {# post}}.", "annual_report.summary.new_posts.new_posts": "nuovi post", "annual_report.summary.percentile.text": "Ciò ti colloca in cimaagli utenti di {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "Non lo diremo a Bernie.", + "annual_report.summary.share_elsewhere": "Condividi altrove", "annual_report.summary.share_message": "Ho ottenuto l'archetipo: {archetype}!", - "annual_report.summary.thanks": "Grazie per far parte di Mastodon!", + "annual_report.summary.share_on_mastodon": "Condividi su Mastodon", "attachments_list.unprocessed": "(non elaborato)", "audio.hide": "Nascondi audio", "block_modal.remote_users_caveat": "Chiederemo al server {domain} di rispettare la tua decisione. Tuttavia, la conformità non è garantita poiché alcuni server potrebbero gestire i blocchi in modo diverso. I post pubblici potrebbero essere ancora visibili agli utenti che non hanno effettuato l'accesso.", @@ -419,6 +441,8 @@ "follow_suggestions.who_to_follow": "Chi seguire", "followed_tags": "Hashtag seguiti", "footer.about": "Info", + "footer.about_mastodon": "Riguardo Mastodon", + "footer.about_server": "Riguardo {domain}", "footer.about_this_server": "Info", "footer.directory": "Cartella dei profili", "footer.get_app": "Scarica l'app", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index c3e5b3d0bb1b0f..8c0bfdc32a20bb 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -127,25 +127,16 @@ "alt_text_modal.describe_for_people_with_visual_impairments": "目が不自由な方のために説明してください…", "alt_text_modal.done": "完了", "announcement.announcement": "お知らせ", - "annual_report.summary.archetype.booster": "トレンドハンター", - "annual_report.summary.archetype.lurker": "ROM専", - "annual_report.summary.archetype.oracle": "予言者", - "annual_report.summary.archetype.pollster": "調査員", - "annual_report.summary.archetype.replier": "社交家", - "annual_report.summary.followers.followers": "フォロワー", - "annual_report.summary.followers.total": "合計{count}", - "annual_report.summary.here_it_is": "こちらがあなたの{year}年の振り返りです", - "annual_report.summary.highlighted_post.by_favourites": "最もお気に入りされた投稿", - "annual_report.summary.highlighted_post.by_reblogs": "最もブーストされた投稿", - "annual_report.summary.highlighted_post.by_replies": "最も返信が多かった投稿", - "annual_report.summary.highlighted_post.possessive": "{name}の", + "annual_report.summary.followers.new_followers": "{count, plural, other {新しいフォロワー}}", + "annual_report.summary.highlighted_post.boost_count": "この投稿は {count, plural, other {# 回}}ブーストされました。", + "annual_report.summary.highlighted_post.favourite_count": "この投稿は {count, plural, other {# 回}}お気に入りされました。", + "annual_report.summary.highlighted_post.title": "最も人気のある投稿", "annual_report.summary.most_used_app.most_used_app": "最も使用されているアプリ", "annual_report.summary.most_used_hashtag.most_used_hashtag": "最も使用されたハッシュタグ", - "annual_report.summary.most_used_hashtag.none": "なし", + "annual_report.summary.most_used_hashtag.used_count": "あなたはこのハッシュタグを {count, plural, other {# 件の投稿}}に含めました。", "annual_report.summary.new_posts.new_posts": "新しい投稿", "annual_report.summary.percentile.text": "{domain}で 上位に入ります!", "annual_report.summary.percentile.we_wont_tell_bernie": "バー二ーには秘密にしておくよ。", - "annual_report.summary.thanks": "Mastodonの一員になってくれてありがとう!", "antennas.accounts": "{count} のアカウント", "antennas.add_domain": "新規ドメイン", "antennas.add_domain_placeholder": "新しいドメイン名", diff --git a/app/javascript/mastodon/locales/kab.json b/app/javascript/mastodon/locales/kab.json index 36ecff0b43cfc6..05ea5f312ef27e 100644 --- a/app/javascript/mastodon/locales/kab.json +++ b/app/javascript/mastodon/locales/kab.json @@ -92,13 +92,9 @@ "alt_text_modal.cancel": "Semmet", "alt_text_modal.done": "Immed", "announcement.announcement": "Ulɣu", - "annual_report.summary.followers.followers": "imeḍfaṛen", - "annual_report.summary.followers.total": "{count} deg aɣrud", "annual_report.summary.most_used_app.most_used_app": "asnas yettwasqedcen s waṭas", - "annual_report.summary.most_used_hashtag.none": "Ula yiwen", "annual_report.summary.new_posts.new_posts": "tisuffaɣ timaynutin", "annual_report.summary.percentile.we_wont_tell_bernie": "Ur as-neqqar i yiwen.", - "annual_report.summary.thanks": "Tanemmirt imi i tettekkiḍ deg Mastodon!", "audio.hide": "Ffer amesli", "block_modal.show_less": "Ssken-d drus", "block_modal.show_more": "Ssken-d ugar", diff --git a/app/javascript/mastodon/locales/kk.json b/app/javascript/mastodon/locales/kk.json index 8e29c3fbeed207..7784f3d9e6871e 100644 --- a/app/javascript/mastodon/locales/kk.json +++ b/app/javascript/mastodon/locales/kk.json @@ -9,6 +9,7 @@ "about.domain_blocks.silenced.title": "Шектеулі", "about.domain_blocks.suspended.explanation": "Бұл сервердің деректері өңделмейді, сақталмайды және айырбасталмайды, сондықтан бұл сервердің қолданушыларымен кез келген әрекеттесу немесе байланыс мүмкін емес.", "about.domain_blocks.suspended.title": "Тоқтатылған", + "about.language_label": "Тіл", "about.not_available": "Бұл ақпарат бұл серверде қолжетімді емес.", "about.powered_by": "{mastodon} негізіндегі орталықсыз әлеуметтік желі", "about.rules": "Сервер ережелері", @@ -19,11 +20,13 @@ "account.block_domain": "{domain} доменін бұғаттау", "account.block_short": "Бұғаттау", "account.blocked": "Бұғатталған", + "account.blocking": "Бұғаттау", "account.cancel_follow_request": "Withdraw follow request", "account.direct": "@{name} жеке айту", "account.disable_notifications": "@{name} постары туралы ескертпеу", "account.domain_blocking": "Доменді бұғаттау", "account.edit_profile": "Профильді өңдеу", + "account.edit_profile_short": "Түзеу", "account.enable_notifications": "@{name} постары туралы ескерту", "account.endorse": "Профильде ұсыну", "account.familiar_followers_many": "{name1}, {name2} және {othersCount, plural, one {сіз білетін тағы бір адам} other {сіз білетін тағы # адам}} жазылған", @@ -91,7 +94,11 @@ "alert.rate_limited.title": "Бағалау шектеулі", "alert.unexpected.message": "Бір нәрсе дұрыс болмады.", "alert.unexpected.title": "Өй!", + "alt_text_badge.title": "Балама мазмұны", + "alt_text_modal.add_alt_text": "Балама мазмұнды қосу", + "alt_text_modal.done": "Дайын", "announcement.announcement": "Хабарландыру", + "annual_report.summary.close": "Жабу", "boost_modal.combo": "Келесіде өткізіп жіберу үшін басыңыз {combo}", "bundle_column_error.retry": "Қайтадан көріңіз", "bundle_modal_error.close": "Жабу", diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json index e3552abd2d3c62..6dfb4948863d6a 100644 --- a/app/javascript/mastodon/locales/ko.json +++ b/app/javascript/mastodon/locales/ko.json @@ -113,25 +113,38 @@ "alt_text_modal.describe_for_people_with_visual_impairments": "시각 장애가 있는 사람들을 위한 설명을 작성하세요…", "alt_text_modal.done": "완료", "announcement.announcement": "공지사항", - "annual_report.summary.archetype.booster": "연쇄부스트마", - "annual_report.summary.archetype.lurker": "은둔자", - "annual_report.summary.archetype.oracle": "예언자", - "annual_report.summary.archetype.pollster": "여론조사원", - "annual_report.summary.archetype.replier": "답글나비", - "annual_report.summary.followers.followers": "팔로워", - "annual_report.summary.followers.total": "총 {count}", - "annual_report.summary.here_it_is": "{year}년 결산입니다:", - "annual_report.summary.highlighted_post.by_favourites": "가장 많은 좋아요를 받은 게시물", - "annual_report.summary.highlighted_post.by_reblogs": "가장 많이 부스트된 게시물", - "annual_report.summary.highlighted_post.by_replies": "가장 많은 답글을 받은 게시물", - "annual_report.summary.highlighted_post.possessive": "{name} 님의", + "annual_report.announcement.action_build": "랩스토돈 만들기", + "annual_report.announcement.action_view": "랩스토돈 보기", + "annual_report.announcement.title": "{year} 랩스토돈이 도착했습니다", + "annual_report.summary.archetype.booster.desc_public": "{name} 님은 부스트할 게시물을 기다리고 정확한 조준으로 다른 제작자들을 밀어주었습니다.", + "annual_report.summary.archetype.booster.desc_self": "당신은 부스트할 게시물을 기다리고 정확한 조준으로 다른 제작자들을 밀어주었습니다.", + "annual_report.summary.archetype.booster.name": "궁수", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.name": "금욕주의자", + "annual_report.summary.archetype.oracle.desc_public": "{name} 님은 답글보다 새로운 글을 많이 작성해 마스토돈을 신선하고 미래지향적으로 만들었습니다.", + "annual_report.summary.archetype.oracle.desc_self": "당신은 답글보다 새로운 글을 많이 작성해 마스토돈을 신선하고 미래지향적으로 만들었습니다.", + "annual_report.summary.archetype.oracle.name": "예언자", + "annual_report.summary.archetype.pollster.desc_public": "{name} 님은 다른 게시물보다 투표를 많이 만들었고 마스토돈에서 호기심을 일궈냈습니다.", + "annual_report.summary.archetype.pollster.desc_self": "당신은 다른 게시물보다 투표를 많이 만들었고 마스토돈에서 호기심을 일궈냈습니다.", + "annual_report.summary.archetype.pollster.name": "호기심쟁이", + "annual_report.summary.archetype.replier.desc_public": "{name} 님은 다른 사람의 게시물에 자주 답글을 남겨 마스토돈에 새로운 논의거리를 만들어냈습니다.", + "annual_report.summary.archetype.replier.desc_self": "당신은 다른 사람의 게시물에 자주 답글을 남겨 마스토돈에 새로운 논의거리를 만들어냈습니다.", + "annual_report.summary.archetype.replier.name": "나비", + "annual_report.summary.archetype.reveal": "내 특성을 확인합니다", + "annual_report.summary.archetype.reveal_description": "마스토돈의 일원이 되어주셔서 감사합니다! {year} 년엔 어떤 특성을 부여받는지 알아봅시다.", + "annual_report.summary.archetype.title_public": "{name} 님의 특성", + "annual_report.summary.archetype.title_self": "당신의 특성", + "annual_report.summary.close": "닫기", + "annual_report.summary.highlighted_post.boost_count": "이 게시물은 {count, plural,other {# 번}} 부스트되었습니다.", + "annual_report.summary.highlighted_post.favourite_count": "이 게시물은 {count, plural,other {# 번}} 마음에 들었습니다.", + "annual_report.summary.highlighted_post.reply_count": "이 게시물은 {count, plural, other {# 개}}의 답글을 받았습니다.", + "annual_report.summary.highlighted_post.title": "가장 인기있는 게시물", "annual_report.summary.most_used_app.most_used_app": "가장 많이 사용한 앱", "annual_report.summary.most_used_hashtag.most_used_hashtag": "가장 많이 사용한 해시태그", - "annual_report.summary.most_used_hashtag.none": "없음", "annual_report.summary.new_posts.new_posts": "새 게시물", "annual_report.summary.percentile.text": "{domain} 사용자의 상위입니다.", "annual_report.summary.percentile.we_wont_tell_bernie": "종부세는 안 걷을게요", - "annual_report.summary.thanks": "마스토돈과 함께 해주셔서 감사합니다!", + "annual_report.summary.share_message": "나는 {archetype} 특성을 부여받았습니다!", "attachments_list.unprocessed": "(처리 안 됨)", "audio.hide": "소리 숨기기", "block_modal.remote_users_caveat": "우리는 {domain} 서버가 당신의 결정을 존중해 주길 부탁할 것입니다. 하지만 몇몇 서버는 차단을 다르게 취급할 수 있기 때문에 규정이 준수되는 것을 보장할 수는 없습니다. 공개 게시물은 로그인 하지 않은 사용자들에게 여전히 보여질 수 있습니다.", @@ -513,6 +526,7 @@ "keyboard_shortcuts.toggle_hidden": "CW로 가려진 텍스트를 표시/비표시", "keyboard_shortcuts.toggle_sensitivity": "미디어 보이기/숨기기", "keyboard_shortcuts.toot": "새 게시물 작성", + "keyboard_shortcuts.top": "목록의 최상단으로 이동", "keyboard_shortcuts.translate": "게시물 번역", "keyboard_shortcuts.unfocus": "작성창에서 포커스 해제", "keyboard_shortcuts.up": "리스트에서 위로 이동", diff --git a/app/javascript/mastodon/locales/ku.json b/app/javascript/mastodon/locales/ku.json index f2efc7f3c68889..778ff2684a5635 100644 --- a/app/javascript/mastodon/locales/ku.json +++ b/app/javascript/mastodon/locales/ku.json @@ -95,9 +95,6 @@ "alt_text_modal.change_thumbnail": "Wêneyê biçûk biguherîne", "alt_text_modal.done": "Qediya", "announcement.announcement": "Daxuyanî", - "annual_report.summary.followers.followers": "şopîner", - "annual_report.summary.followers.total": "{count} tevahî", - "annual_report.summary.most_used_hashtag.none": "Ne yek", "annual_report.summary.new_posts.new_posts": "şandiyên nû", "attachments_list.unprocessed": "(bêpêvajo)", "audio.hide": "Dengê veşêre", diff --git a/app/javascript/mastodon/locales/lad.json b/app/javascript/mastodon/locales/lad.json index 9e6f503983289e..4ba1450029c0df 100644 --- a/app/javascript/mastodon/locales/lad.json +++ b/app/javascript/mastodon/locales/lad.json @@ -109,18 +109,9 @@ "alt_text_modal.change_thumbnail": "Troka minyatura", "alt_text_modal.done": "Fecho", "announcement.announcement": "Pregon", - "annual_report.summary.archetype.pollster": "El anketero", - "annual_report.summary.followers.followers": "suivantes", - "annual_report.summary.followers.total": "{count} en total", - "annual_report.summary.highlighted_post.by_favourites": "la puvlikasyon mas favoritada", - "annual_report.summary.highlighted_post.by_reblogs": "la puvlikasyon mas repartajada", - "annual_report.summary.highlighted_post.by_replies": "la puvlikasyon kon mas repuestas", - "annual_report.summary.highlighted_post.possessive": "de {name}", "annual_report.summary.most_used_app.most_used_app": "la aplikasyon mas uzada", "annual_report.summary.most_used_hashtag.most_used_hashtag": "la etiketa mas uzada", - "annual_report.summary.most_used_hashtag.none": "Dinguno", "annual_report.summary.new_posts.new_posts": "puvlikasyones muevas", - "annual_report.summary.thanks": "Mersi por ser parte de Mastodon!", "attachments_list.unprocessed": "(no prosesado)", "audio.hide": "Eskonde audio", "block_modal.show_less": "Amostra manko", diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json index f9edb7bfb55dd9..f6da5eb6326b16 100644 --- a/app/javascript/mastodon/locales/lt.json +++ b/app/javascript/mastodon/locales/lt.json @@ -117,26 +117,12 @@ "annual_report.announcement.action_view": "Peržiūrėti mano Wrapstodon", "annual_report.announcement.description": "Sužinokite daugiau apie savo aktyvumą Mastodon\"e per pastaruosius metus.", "annual_report.announcement.title": "Wrapstodon {year} jau čia", - "annual_report.summary.archetype.booster": "Šaunus medžiotojas", - "annual_report.summary.archetype.lurker": "Stebėtojas", - "annual_report.summary.archetype.oracle": "Vydūnas", - "annual_report.summary.archetype.pollster": "Apklausos rengėjas", - "annual_report.summary.archetype.replier": "Socialinis drugelis", - "annual_report.summary.followers.followers": "sekėjai (-ų)", - "annual_report.summary.followers.total": "iš viso {count}", - "annual_report.summary.here_it_is": "Štai jūsų {year} apžvalga:", - "annual_report.summary.highlighted_post.by_favourites": "labiausiai pamėgtas įrašas", - "annual_report.summary.highlighted_post.by_reblogs": "labiausiai pasidalintas įrašas", - "annual_report.summary.highlighted_post.by_replies": "įrašas su daugiausiai atsakymų", - "annual_report.summary.highlighted_post.possessive": "{name}", "annual_report.summary.most_used_app.most_used_app": "labiausiai naudota programa", "annual_report.summary.most_used_hashtag.most_used_hashtag": "labiausiai naudota grotažymė", - "annual_report.summary.most_used_hashtag.none": "Nieko", "annual_report.summary.new_posts.new_posts": "nauji įrašai", "annual_report.summary.percentile.text": "Tai reiškia, kad esate tarppopuliariausių {domain} naudotojų.", "annual_report.summary.percentile.we_wont_tell_bernie": "Mes nesakysime Bernie.", "annual_report.summary.share_message": "Aš gavau „{archetype}“!", - "annual_report.summary.thanks": "Dėkojame, kad esate „Mastodon“ dalis!", "attachments_list.unprocessed": "(neapdorotas)", "audio.hide": "Slėpti garsą", "block_modal.remote_users_caveat": "Paprašysime serverio {domain} gerbti tavo sprendimą. Tačiau atitiktis negarantuojama, nes kai kurie serveriai gali skirtingai tvarkyti blokavimus. Vieši įrašai vis tiek gali būti matomi neprisijungusiems naudotojams.", diff --git a/app/javascript/mastodon/locales/lv.json b/app/javascript/mastodon/locales/lv.json index eeda75d85d190c..260b4486b85cab 100644 --- a/app/javascript/mastodon/locales/lv.json +++ b/app/javascript/mastodon/locales/lv.json @@ -113,23 +113,11 @@ "alt_text_modal.describe_for_people_with_visual_impairments": "Aprakstīt šo cilvēkiem ar redzes traucējumiem…", "alt_text_modal.done": "Gatavs", "announcement.announcement": "Paziņojums", - "annual_report.summary.archetype.lurker": "Glūņa", - "annual_report.summary.archetype.oracle": "Orākuls", - "annual_report.summary.archetype.replier": "Sabiedriskais tauriņš", - "annual_report.summary.followers.followers": "sekotāji", - "annual_report.summary.followers.total": "pavisam {count}", - "annual_report.summary.here_it_is": "Šeit ir {year}. gada pārskats:", - "annual_report.summary.highlighted_post.by_favourites": "izlasēm visvairāk pievienotais ieraksts", - "annual_report.summary.highlighted_post.by_reblogs": "vispastiprinātākais ieraksts", - "annual_report.summary.highlighted_post.by_replies": "ieraksts ar vislielāko atbilžu skaitu", - "annual_report.summary.highlighted_post.possessive": "{name}", "annual_report.summary.most_used_app.most_used_app": "visizmantotākā lietotne", "annual_report.summary.most_used_hashtag.most_used_hashtag": "visizmantotākais tēmturis", - "annual_report.summary.most_used_hashtag.none": "Nav", "annual_report.summary.new_posts.new_posts": "jauni ieraksti", "annual_report.summary.percentile.text": "Tas ievieto Tevi virsējosno {domain} lietotājiem.", "annual_report.summary.percentile.we_wont_tell_bernie": "Mēs neteiksim Bērnijam.", - "annual_report.summary.thanks": "Paldies, ka esi daļa no Mastodon!", "attachments_list.unprocessed": "(neapstrādāti)", "audio.hide": "Slēpt audio", "block_modal.remote_users_caveat": "Mēs lūgsim serverim {domain} ņemt vērā Tavu lēmumu. Tomēr sadarbība nav garantēta, jo atsevišķi serveri bloķēšanu var apstrādāt citādi. Publiski ieraksti, iespējams, joprojām būs redzami lietotājiem, kuri nav pieteikušies.", @@ -244,7 +232,9 @@ "confirmations.missing_alt_text.title": "Pievienot aprakstošo tekstu?", "confirmations.mute.confirm": "Apklusināt", "confirmations.private_quote_notify.cancel": "Atgriezties pie labošanas", + "confirmations.private_quote_notify.confirm": "Publicēt ierakstu", "confirmations.private_quote_notify.do_not_show_again": "Nerādīt vairāk šo paziņojumu", + "confirmations.private_quote_notify.title": "Dalīties ar sekotājiem un pieminētajiem lietotājiem?", "confirmations.quiet_post_quote_info.got_it": "Sapratu", "confirmations.redraft.confirm": "Dzēst un pārrakstīt", "confirmations.redraft.message": "Vai tiešām vēlies izdzēst šo ierakstu un veidot jaunu tā uzmetumu? Pievienošana izlasēs un pastiprinājumi tiks zaudēti, un sākotnējā ieraksta atbildes paliks bez saiknes ar to.", @@ -423,6 +413,9 @@ "home.pending_critical_update.title": "Ir pieejams būtisks drošības atjauninājums.", "home.show_announcements": "Rādīt paziņojumus", "ignore_notifications_modal.ignore": "Neņemt vērā paziņojumus", + "ignore_notifications_modal.limited_accounts_title": "Neņemt vērā paziņojumus no moderētiem kontiem?", + "ignore_notifications_modal.new_accounts_title": "Neņemt vērā paziņojumus no jauniem kontiem?", + "ignore_notifications_modal.not_followers_title": "Neņemt vērā paziņojumus no cilvēkiem, kas tev neseko?", "ignore_notifications_modal.not_following_title": "Neņemt vērā paziņojumus no cilvēkiem, kuriem neseko?", "info_button.label": "Palīdzība", "interaction_modal.go": "Aiziet", @@ -457,6 +450,7 @@ "keyboard_shortcuts.open_media": "Atvērt multividi", "keyboard_shortcuts.pinned": "Atvērt piesprausto ierakstu sarakstu", "keyboard_shortcuts.profile": "Atvērt autora profilu", + "keyboard_shortcuts.quote": "Citēt ierakstu", "keyboard_shortcuts.reply": "Atbildēt", "keyboard_shortcuts.requests": "Atvērt sekošanas pieprasījumu sarakstu", "keyboard_shortcuts.search": "Fokusēt meklēšanas joslu", diff --git a/app/javascript/mastodon/locales/ms.json b/app/javascript/mastodon/locales/ms.json index 46011b09a82087..71013cf3e6db15 100644 --- a/app/javascript/mastodon/locales/ms.json +++ b/app/javascript/mastodon/locales/ms.json @@ -94,25 +94,11 @@ "alt_text_modal.describe_for_people_with_visual_impairments": "Terangkan untuk OKU penglihatan…", "alt_text_modal.done": "Selesai", "announcement.announcement": "Pengumuman", - "annual_report.summary.archetype.booster": "Si pencapap", - "annual_report.summary.archetype.lurker": "Si penghendap", - "annual_report.summary.archetype.oracle": "Si penilik", - "annual_report.summary.archetype.pollster": "Si peninjau", - "annual_report.summary.archetype.replier": "Si peramah", - "annual_report.summary.followers.followers": "pengikut", - "annual_report.summary.followers.total": "sebanyak {count}", - "annual_report.summary.here_it_is": "Ini ulasan {year} anda:", - "annual_report.summary.highlighted_post.by_favourites": "hantaran paling disukai ramai", - "annual_report.summary.highlighted_post.by_reblogs": "hantaran paling digalak ramai", - "annual_report.summary.highlighted_post.by_replies": "hantaran paling dibalas ramai", - "annual_report.summary.highlighted_post.possessive": "oleh", "annual_report.summary.most_used_app.most_used_app": "aplikasi paling banyak digunakan", "annual_report.summary.most_used_hashtag.most_used_hashtag": "tanda pagar paling banyak digunakan", - "annual_report.summary.most_used_hashtag.none": "Tiada", "annual_report.summary.new_posts.new_posts": "hantaran baharu", "annual_report.summary.percentile.text": "Anda berkedudukan pengguna {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "Rahsia anda selamat bersama kami. ;)", - "annual_report.summary.thanks": "Terima kasih kerana setia bersama Mastodon!", "attachments_list.unprocessed": "(belum diproses)", "audio.hide": "Sembunyikan audio", "block_modal.remote_users_caveat": "Kami akan meminta pelayan {domain} untuk menghormati keputusan anda. Bagaimanapun, pematuhan tidak dijamin kerana ada pelayan yang mungkin menangani sekatan dengan cara berbeza. Hantaran awam mungkin masih tampak kepada pengguna yang tidak log masuk.", diff --git a/app/javascript/mastodon/locales/nan.json b/app/javascript/mastodon/locales/nan.json index 9f5e53ef2449e3..366f568f0a44b3 100644 --- a/app/javascript/mastodon/locales/nan.json +++ b/app/javascript/mastodon/locales/nan.json @@ -114,29 +114,50 @@ "alt_text_modal.done": "做好ah", "announcement.announcement": "公告", "annual_report.announcement.action_build": "建立我ê Wrapstodon", + "annual_report.announcement.action_dismiss": "毋免,多謝。", "annual_report.announcement.action_view": "看我ê Wrapstodon", "annual_report.announcement.description": "發現其他關係lí佇最近tsi̍t年參與Mastodon ê狀況。", "annual_report.announcement.title": "Wrapstodon {year} 已經kàu ah", - "annual_report.summary.archetype.booster": "追求趣味ê", - "annual_report.summary.archetype.lurker": "有讀無PO ê", - "annual_report.summary.archetype.oracle": "先知", - "annual_report.summary.archetype.pollster": "愛發動投票ê", - "annual_report.summary.archetype.replier": "社交ê蝴蝶", - "annual_report.summary.followers.followers": "跟tuè lí ê", - "annual_report.summary.followers.total": "Lóng總有 {count} ê", - "annual_report.summary.here_it_is": "下kha是lí {year} 年ê回顧:", - "annual_report.summary.highlighted_post.by_favourites": "Hōo足tsē lâng收藏ê PO文", - "annual_report.summary.highlighted_post.by_reblogs": "Hōo足tsē lâng轉ê PO文", - "annual_report.summary.highlighted_post.by_replies": "有上tsē回應ê PO文", - "annual_report.summary.highlighted_post.possessive": "{name} ê", + "annual_report.nav_item.badge": "新ê", + "annual_report.shared_page.donate": "寄付", + "annual_report.shared_page.footer": "由 Mastodon 團隊用 {heart} 產生", + "annual_report.summary.archetype.booster.desc_public": "{name} 不斷走tshuē通轉送 ê PO文,靠完美ê對千,放大其他ê創作者。", + "annual_report.summary.archetype.booster.desc_self": " Lí不斷走tshuē通轉送 ê PO文,靠完美ê對千,放大其他ê創作者。", + "annual_report.summary.archetype.booster.name": "射手", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.desc_public": "Guán知 {name} 捌佇某物所在,用伊恬靜ê方法享受Mastodon。", + "annual_report.summary.archetype.lurker.desc_self": "Guán知lí捌佇某物所在,用li恬靜ê方法享受Mastodon。", + "annual_report.summary.archetype.lurker.name": "Stoic主義者", + "annual_report.summary.archetype.oracle.desc_public": "{name} 發表ê新ê PO文較tsē回應,保持 Mastodon tshinn-tshioh kap面對未來。", + "annual_report.summary.archetype.oracle.desc_self": "Lí發表ê新ê PO文較tsē回應,保持 Mastodon tshinn-tshioh kap面對未來。", + "annual_report.summary.archetype.oracle.name": "先知", + "annual_report.summary.archetype.pollster.desc_public": "{name} 建立投票較tsē其他PO文類型,佇Mastodon培養好奇。", + "annual_report.summary.archetype.pollster.desc_self": "Lí建立投票較tsē其他PO文類型,佇Mastodon培養好奇。", + "annual_report.summary.archetype.pollster.name": "思考者", + "annual_report.summary.archetype.replier.desc_public": "{name} 定定應別lâng ê PO文,用新ê討論kā Mastodon授粉。", + "annual_report.summary.archetype.replier.desc_self": "You 定定應別lâng ê PO文,用新ê討論kā Mastodon授粉。", + "annual_report.summary.archetype.replier.name": "Ia̍h仔", + "annual_report.summary.archetype.reveal": "顯露我ê原形", + "annual_report.summary.archetype.reveal_description": "感謝lí成做Mastodon ê一部份!Tsit-má tshuē出lí佇 {year} 年表現ê原形。", + "annual_report.summary.archetype.title_public": "{name} ê原形", + "annual_report.summary.archetype.title_self": "Lí ê原形", + "annual_report.summary.close": "關", + "annual_report.summary.copy_link": "Khóo-pih連結", + "annual_report.summary.followers.new_followers": "{count, plural, other {新ê跟tuè ê}}", + "annual_report.summary.highlighted_post.boost_count": "Tsit篇PO文予lâng轉送 {count, plural, other {# kái}}。", + "annual_report.summary.highlighted_post.favourite_count": "Tsit篇PO文予lâng收藏 {count, plural, other {# kái}}。", + "annual_report.summary.highlighted_post.reply_count": "Tsit篇PO文得著 {count, plural, other {# 篇回應}}。", + "annual_report.summary.highlighted_post.title": "上hang ê PO文", "annual_report.summary.most_used_app.most_used_app": "上tsē lâng用ê app", "annual_report.summary.most_used_hashtag.most_used_hashtag": "上tsia̍p用ê hashtag", - "annual_report.summary.most_used_hashtag.none": "無", + "annual_report.summary.most_used_hashtag.used_count": "Lí佇{count, plural, other { # 篇PO文}}內包含tsit ê hashtag。", + "annual_report.summary.most_used_hashtag.used_count_public": "{name} 佇{count, plural, other { # 篇PO文}}內包含tsit ê hashtag。", "annual_report.summary.new_posts.new_posts": "新ê PO文", "annual_report.summary.percentile.text": "Tse 予lí變做 {domain} ê用戶ê ", "annual_report.summary.percentile.we_wont_tell_bernie": "Gún bē kā Bernie講。", + "annual_report.summary.share_elsewhere": "分享kàu別位", "annual_report.summary.share_message": "我得著 {archetype} ê典型!", - "annual_report.summary.thanks": "多謝成做Mastodon ê成員!", + "annual_report.summary.share_on_mastodon": "佇Mastodon分享", "attachments_list.unprocessed": "(Iáu bē處理)", "audio.hide": "Tshàng聲音", "block_modal.remote_users_caveat": "Guán ē要求服侍器 {domain} 尊重lí ê決定。但是bô法度保證ta̍k ê服侍器lóng遵守,因為tsi̍t-kuá服侍器huân-sè用別款方法處理封鎖。公開ê PO文可能iáu是ē hōo bô登入ê用者看著。", diff --git a/app/javascript/mastodon/locales/ne.json b/app/javascript/mastodon/locales/ne.json index 5b6b0994864ab3..2d861e470be3a1 100644 --- a/app/javascript/mastodon/locales/ne.json +++ b/app/javascript/mastodon/locales/ne.json @@ -77,8 +77,6 @@ "alert.unexpected.message": "एउटा अनपेक्षित त्रुटि भयो।", "alt_text_modal.cancel": "रद्द गर्नुहोस्", "announcement.announcement": "घोषणा", - "annual_report.summary.followers.followers": "फलोअरहरु", - "annual_report.summary.highlighted_post.by_reblogs": "सबैभन्दा बढि बूस्ट गरिएको पोस्ट", "annual_report.summary.new_posts.new_posts": "नयाँ पोस्टहरू", "block_modal.remote_users_caveat": "हामी सर्भर {domain} लाई तपाईंको निर्णयको सम्मान गर्न सोध्नेछौं। तर, हामी अनुपालनको ग्यारेन्टी दिन सक्दैनौं किनभने केही सर्भरहरूले ब्लकहरू फरक रूपमा ह्यान्डल गर्न सक्छन्। सार्वजनिक पोस्टहरू लग इन नभएका प्रयोगकर्ताहरूले देख्न सक्छन्।", "block_modal.show_less": "कम देखाउनुहोस्", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index 28406163ba40e4..792f6afd1dd016 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -114,29 +114,51 @@ "alt_text_modal.done": "Klaar", "announcement.announcement": "Mededeling", "annual_report.announcement.action_build": "Bouw mijn Wrapstodon", + "annual_report.announcement.action_dismiss": "Nee, bedankt", "annual_report.announcement.action_view": "Bekijk mijn Wrapstodon", "annual_report.announcement.description": "Ontdek meer over jouw engagement op Mastodon over het afgelopen jaar.", "annual_report.announcement.title": "Wrapstodon {year} is gearriveerd", - "annual_report.summary.archetype.booster": "De cool-hunter", - "annual_report.summary.archetype.lurker": "De lurker", - "annual_report.summary.archetype.oracle": "Het orakel", - "annual_report.summary.archetype.pollster": "De opiniepeiler", - "annual_report.summary.archetype.replier": "De sociale vlinder", - "annual_report.summary.followers.followers": "volgers", - "annual_report.summary.followers.total": "totaal {count}", - "annual_report.summary.here_it_is": "Hier is jouw terugblik op {year}:", - "annual_report.summary.highlighted_post.by_favourites": "bericht met de meeste favorieten", - "annual_report.summary.highlighted_post.by_reblogs": "bericht met de meeste boosts", - "annual_report.summary.highlighted_post.by_replies": "bericht met de meeste reacties", - "annual_report.summary.highlighted_post.possessive": "{name}'s", + "annual_report.nav_item.badge": "Nieuw", + "annual_report.shared_page.donate": "Doneren", + "annual_report.shared_page.footer": "Met {heart} gegenereerd door het Mastodonteam", + "annual_report.shared_page.footer_server_info": "{username} gebruikt {domain}, een van de vele door Mastodon mogelijk gemaakte gemeenschappen.", + "annual_report.summary.archetype.booster.desc_public": "{name} bleef op jacht naar berichten om te kunnen boosten, waardoor het geluid van andere gebruikers werd versterkt.", + "annual_report.summary.archetype.booster.desc_self": "Jij bleef op jacht naar berichten om te kunnen boosten, waardoor je het geluid van andere gebruikers versterkte.", + "annual_report.summary.archetype.booster.name": "De Boogschutter", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.desc_public": "We weten dat {name} ergens in stilte van Mastodon aan het genieten was.", + "annual_report.summary.archetype.lurker.desc_self": "We weten dat je ergens in stilte van Mastodon aan het genieten was.", + "annual_report.summary.archetype.lurker.name": "De Stoïcijn", + "annual_report.summary.archetype.oracle.desc_public": "{name} heeft meer nieuwe berichten dan reacties geplaatst, waardoor Mastodon fris en toekomstgericht blijft.", + "annual_report.summary.archetype.oracle.desc_self": "Je hebt meer nieuwe berichten dan reacties geplaatst, waardoor Mastodon fris en toekomstgericht blijft.", + "annual_report.summary.archetype.oracle.name": "Het Orakel", + "annual_report.summary.archetype.pollster.desc_public": "{name} heeft meer polls aangemaakt dan andere soorten berichten, en daarmee de nieuwsgierigheid op Mastodon gecultiveerd.", + "annual_report.summary.archetype.pollster.desc_self": "Je hebt meer polls aangemaakt dan andere soorten berichten, en daarmee de nieuwsgierigheid op Mastodon gecultiveerd.", + "annual_report.summary.archetype.pollster.name": "De Dromer", + "annual_report.summary.archetype.replier.desc_public": "{name} reageerde regelmatig op berichten van andere mensen, waardoor nieuwe discussies op Mastodon tot bloei kwamen.", + "annual_report.summary.archetype.replier.desc_self": "Je reageerde regelmatig op berichten van andere mensen, waardoor nieuwe discussies op Mastodon tot bloei kwamen.", + "annual_report.summary.archetype.replier.name": "De Vlinder", + "annual_report.summary.archetype.reveal": "Onthul mijn archetype", + "annual_report.summary.archetype.reveal_description": "Bedankt dat je deel uitmaakt van Mastodon! Tijd om te ontdekken welk archetype je in {year} belichaamde.", + "annual_report.summary.archetype.title_public": "Het archetype van {name}", + "annual_report.summary.archetype.title_self": "Jouw archetype", + "annual_report.summary.close": "Sluiten", + "annual_report.summary.copy_link": "Link kopiëren", + "annual_report.summary.followers.new_followers": "{count, plural, one {nieuwe volger} other {nieuwe volgers}}", + "annual_report.summary.highlighted_post.boost_count": "Dit bericht is {count, plural, one {één keer} other {# keer}} geboost.", + "annual_report.summary.highlighted_post.favourite_count": "Dit bericht is {count, plural, one {één keer} other {# keer}} als favoriet gemarkeerd.", + "annual_report.summary.highlighted_post.reply_count": "Dit bericht heeft {count, plural, one {één reactie} other {# reacties}}.", + "annual_report.summary.highlighted_post.title": "Meest populaire berichten", "annual_report.summary.most_used_app.most_used_app": "meest gebruikte app", "annual_report.summary.most_used_hashtag.most_used_hashtag": "meest gebruikte hashtag", - "annual_report.summary.most_used_hashtag.none": "Geen", + "annual_report.summary.most_used_hashtag.used_count": "Je hebt deze hashtag in {count, plural, one{één bericht} other {# berichten}} gebruikt.", + "annual_report.summary.most_used_hashtag.used_count_public": "{name} heeft deze hashtag in {count, plural, one {één bericht} other {# berichten}} gebruikt.", "annual_report.summary.new_posts.new_posts": "nieuwe berichten", "annual_report.summary.percentile.text": "Hiermee behoor je tot de top van {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "We zullen Bernie niets vertellen.", + "annual_report.summary.share_elsewhere": "Ergens anders delen", "annual_report.summary.share_message": "Ik heb het archetype {archetype}!", - "annual_report.summary.thanks": "Bedankt dat je deel uitmaakt van Mastodon!", + "annual_report.summary.share_on_mastodon": "Op Mastodon delen", "attachments_list.unprocessed": "(niet verwerkt)", "audio.hide": "Audio verbergen", "block_modal.remote_users_caveat": "We vragen de server {domain} om je besluit te respecteren. Het naleven hiervan is echter niet gegarandeerd, omdat sommige servers blokkades anders kunnen interpreteren. Openbare berichten zijn mogelijk nog steeds zichtbaar voor niet-ingelogde gebruikers.", @@ -419,6 +441,8 @@ "follow_suggestions.who_to_follow": "Wie te volgen", "followed_tags": "Gevolgde hashtags", "footer.about": "Over", + "footer.about_mastodon": "Over Mastodon", + "footer.about_server": "Over {domain}", "footer.about_this_server": "Over", "footer.directory": "Gebruikersgids", "footer.get_app": "App downloaden", diff --git a/app/javascript/mastodon/locales/nn.json b/app/javascript/mastodon/locales/nn.json index 6180bd4c6558e1..7ab269682bf529 100644 --- a/app/javascript/mastodon/locales/nn.json +++ b/app/javascript/mastodon/locales/nn.json @@ -117,25 +117,11 @@ "annual_report.announcement.action_view": "Sjå min Årstodon", "annual_report.announcement.description": "Sjå meir om kva du har gjort på Mastodon siste året.", "annual_report.announcement.title": "Årstodon {year} er her", - "annual_report.summary.archetype.booster": "Den som jaktar på noko kult", - "annual_report.summary.archetype.lurker": "Den som heng på hjørnet", - "annual_report.summary.archetype.oracle": "Orakelet", - "annual_report.summary.archetype.pollster": "Meiningsmålaren", - "annual_report.summary.archetype.replier": "Den sosiale sumarfuglen", - "annual_report.summary.followers.followers": "fylgjarar", - "annual_report.summary.followers.total": "{count} i alt", - "annual_report.summary.here_it_is": "Her er eit gjensyn med {year}:", - "annual_report.summary.highlighted_post.by_favourites": "det mest omtykte innlegget", - "annual_report.summary.highlighted_post.by_reblogs": "det mest framheva innlegget", - "annual_report.summary.highlighted_post.by_replies": "innlegget med flest svar", - "annual_report.summary.highlighted_post.possessive": "som {name} laga", "annual_report.summary.most_used_app.most_used_app": "mest brukte app", "annual_report.summary.most_used_hashtag.most_used_hashtag": "mest brukte emneknagg", - "annual_report.summary.most_used_hashtag.none": "Ingen", "annual_report.summary.new_posts.new_posts": "nye innlegg", "annual_report.summary.percentile.text": "Du er av deiivrigaste brukarane på {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "Ikkje eit ord til pressa.", - "annual_report.summary.thanks": "Takk for at du er med i Mastodon!", "attachments_list.unprocessed": "(ubehandla)", "audio.hide": "Gøym lyd", "block_modal.remote_users_caveat": "Me vil be tenaren {domain} om å respektera di avgjerd. Me kan ikkje garantera at det vert gjort, sidan nokre tenarar kan handtera blokkering ulikt. Offentlege innlegg kan framleis vera synlege for ikkje-innlogga brukarar.", diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json index 8cb67c97b06612..8a2ce940eadaa4 100644 --- a/app/javascript/mastodon/locales/no.json +++ b/app/javascript/mastodon/locales/no.json @@ -107,25 +107,11 @@ "alt_text_modal.describe_for_people_with_visual_impairments": "Beskriv dette for folk med synsproblemer…", "alt_text_modal.done": "Ferdig", "announcement.announcement": "Kunngjøring", - "annual_report.summary.archetype.booster": "Den iskalde jeger", - "annual_report.summary.archetype.lurker": "Det snikende ullteppe", - "annual_report.summary.archetype.oracle": "Allviteren", - "annual_report.summary.archetype.pollster": "Meningsmåleren", - "annual_report.summary.archetype.replier": "Festens midtpunkt", - "annual_report.summary.followers.followers": "følgere", - "annual_report.summary.followers.total": "{count} tilsammen", - "annual_report.summary.here_it_is": "Her er ditt {year} ditt sett sammlet:", - "annual_report.summary.highlighted_post.by_favourites": "mest likte innlegg", - "annual_report.summary.highlighted_post.by_reblogs": "mest opphøyde innlegg", - "annual_report.summary.highlighted_post.by_replies": "innlegg med de fleste tilbakemeldinger", - "annual_report.summary.highlighted_post.possessive": "{name}", "annual_report.summary.most_used_app.most_used_app": "mest brukte applikasjoner", "annual_report.summary.most_used_hashtag.most_used_hashtag": "mest brukte evne knagg", - "annual_report.summary.most_used_hashtag.none": "Ingen", "annual_report.summary.new_posts.new_posts": "nye innlegg", "annual_report.summary.percentile.text": "Det gjør at du er i toppav brukere på {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "Vi skal ikke si noe til Bernie.", - "annual_report.summary.thanks": "Takk for at du er med på Mastodon!", "attachments_list.unprocessed": "(ubehandlet)", "audio.hide": "Skjul lyd", "block_modal.remote_users_caveat": "Vi vil be serveren {domain} om å respektere din beslutning. Det er imidlertid ingen garanti at det blir overholdt, siden noen servere kan håndtere blokkeringer på forskjellig vis. Offentlige innlegg kan fortsatt være synlige for ikke-innloggede brukere.", diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json index 549203306bc2e3..d1d8daa2115423 100644 --- a/app/javascript/mastodon/locales/oc.json +++ b/app/javascript/mastodon/locales/oc.json @@ -91,8 +91,6 @@ "alt_text_modal.change_thumbnail": "Cambiar de miniatura", "alt_text_modal.done": "Acabat", "announcement.announcement": "Anóncia", - "annual_report.summary.followers.followers": "seguidors", - "annual_report.summary.followers.total": "{count} en total", "attachments_list.unprocessed": "(pas tractat)", "audio.hide": "Amagar àudio", "block_modal.show_less": "Ne veire mens", diff --git a/app/javascript/mastodon/locales/pa.json b/app/javascript/mastodon/locales/pa.json index 5d49bfa40a1451..3f519d8c0959e2 100644 --- a/app/javascript/mastodon/locales/pa.json +++ b/app/javascript/mastodon/locales/pa.json @@ -85,16 +85,8 @@ "alt_text_modal.cancel": "ਰੱਦ ਕਰੋ", "alt_text_modal.done": "ਮੁਕੰਮਲ", "announcement.announcement": "ਹੋਕਾ", - "annual_report.summary.followers.followers": "ਫ਼ਾਲੋਅਰ", - "annual_report.summary.followers.total": "{count} ਕੁੱਲ", - "annual_report.summary.highlighted_post.by_favourites": "ਸਭ ਤੋਂ ਵੱਧ ਪਸੰਦ ਕੀਤੀ ਪੋਸਟ", - "annual_report.summary.highlighted_post.by_reblogs": "ਸਭ ਤੋਂ ਵੱਧ ਬੂਸਟ ਕੀਤੀ ਪੋਸਟ", - "annual_report.summary.highlighted_post.by_replies": "ਸਭ ਤੋਂ ਵੱਧ ਜਵਾਬ ਦਿੱਤੀ ਗਈ ਪੋਸਟ", - "annual_report.summary.highlighted_post.possessive": "{name}", "annual_report.summary.most_used_app.most_used_app": "ਸਭ ਤੋਂ ਵੱਧ ਵਰਤੀ ਐਪ", - "annual_report.summary.most_used_hashtag.none": "ਕੋਈ ਨਹੀਂ", "annual_report.summary.new_posts.new_posts": "ਨਵੀਆਂ ਪੋਸਟਾਂ", - "annual_report.summary.thanks": "Mastodon ਦਾ ਹਿੱਸਾ ਬਣਨ ਵਾਸਤੇ ਧੰਨਵਾਦ ਹੈ!", "audio.hide": "ਆਡੀਓ ਨੂੰ ਲੁਕਾਓ", "block_modal.show_less": "ਘੱਟ ਦਿਖਾਓ", "block_modal.show_more": "ਵੱਧ ਦਿਖਾਓ", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index 8f2d33c5fa6875..a6fd027f7ac3d9 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -113,25 +113,11 @@ "alt_text_modal.describe_for_people_with_visual_impairments": "Opisz to dla osób niedowidzących…", "alt_text_modal.done": "Gotowe", "announcement.announcement": "Ogłoszenie", - "annual_report.summary.archetype.booster": "Łowca treści", - "annual_report.summary.archetype.lurker": "Czyhający", - "annual_report.summary.archetype.oracle": "Wyrocznia", - "annual_report.summary.archetype.pollster": "Ankieter", - "annual_report.summary.archetype.replier": "Towarzyski motyl", - "annual_report.summary.followers.followers": "obserwujących", - "annual_report.summary.followers.total": "łącznie {count}", - "annual_report.summary.here_it_is": "Oto przegląd twojego {year} roku:", - "annual_report.summary.highlighted_post.by_favourites": "najbardziej lubiany wpis", - "annual_report.summary.highlighted_post.by_reblogs": "najczęściej podbijany wpis", - "annual_report.summary.highlighted_post.by_replies": "wpis z największą liczbą komentarzy", - "annual_report.summary.highlighted_post.possessive": "{name}", "annual_report.summary.most_used_app.most_used_app": "najczęściej używana aplikacja", "annual_report.summary.most_used_hashtag.most_used_hashtag": "najczęściej używany hashtag", - "annual_report.summary.most_used_hashtag.none": "Brak", "annual_report.summary.new_posts.new_posts": "nowe wpisy", "annual_report.summary.percentile.text": "To plasuje cię w czołówce użytkowników {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "Nie powiemy Berniemu.", - "annual_report.summary.thanks": "Dziękujemy, że jesteś częścią Mastodona!", "attachments_list.unprocessed": "(nieprzetworzone)", "audio.hide": "Ukryj dźwięk", "block_modal.remote_users_caveat": "Poprosimy serwer {domain} o uszanowanie twojej decyzji. Nie jest to jednak gwarantowane, bo niektóre serwery mogą obsługiwać blokady w inny sposób. Publiczne wpisy mogą być nadal widoczne dla niezalogowanych użytkowników.", diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index 158da029d71881..8fdd502accb36a 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -11,7 +11,7 @@ "about.domain_blocks.suspended.title": "Suspenso", "about.language_label": "Idioma", "about.not_available": "Esta informação não foi disponibilizada neste servidor.", - "about.powered_by": "Redes sociais descentralizadas alimentadas por {mastodon}", + "about.powered_by": "Rede social descentralizada baseada no {mastodon}", "about.rules": "Regras do servidor", "account.account_note_header": "Nota pessoal", "account.add_or_remove_from_list": "Adicionar ou remover de listas", @@ -55,8 +55,8 @@ "account.follows.empty": "Nada aqui.", "account.follows_you": "Segue você", "account.go_to_profile": "Ir ao perfil", - "account.hide_reblogs": "Ocultar impulsionamentos de @{name}", - "account.in_memoriam": "Em memória.", + "account.hide_reblogs": "Ocultar impulsos de @{name}", + "account.in_memoriam": "In Memoriam.", "account.joined_short": "Entrou", "account.languages": "Mudar idiomas inscritos", "account.link_verified_on": "A propriedade deste link foi verificada em {date}", @@ -79,7 +79,7 @@ "account.requested_follow": "{name} quer te seguir", "account.requests_to_follow_you": "Pediu para seguir você", "account.share": "Compartilhar perfil de @{name}", - "account.show_reblogs": "Mostrar impulsionamentos de @{name}", + "account.show_reblogs": "Mostrar impulsos de @{name}", "account.statuses_counter": "{count, plural, one {{counter} publicação} other {{counter} publicações}}", "account.unblock": "Desbloquear @{name}", "account.unblock_domain": "Desbloquear domínio {domain}", @@ -106,36 +106,59 @@ "alert.unexpected.title": "Eita!", "alt_text_badge.title": "Texto alternativo", "alt_text_modal.add_alt_text": "Adicione texto alternativo", - "alt_text_modal.add_text_from_image": "Adicione texto da imagem", + "alt_text_modal.add_text_from_image": "Adicione texto a partir da imagem", "alt_text_modal.cancel": "Cancelar", "alt_text_modal.change_thumbnail": "Alterar miniatura", - "alt_text_modal.describe_for_people_with_hearing_impairments": "Descreva isso para pessoas com deficiências auditivas…", + "alt_text_modal.describe_for_people_with_hearing_impairments": "Descreva isto para pessoas com deficiências auditivas…", "alt_text_modal.describe_for_people_with_visual_impairments": "Descreva isto para pessoas com deficiências visuais…", "alt_text_modal.done": "Feito", "announcement.announcement": "Comunicados", "annual_report.announcement.action_build": "Gerar meu Wrapstodon", + "annual_report.announcement.action_dismiss": "Não, obrigado/a", "annual_report.announcement.action_view": "Ver meu Wrapstodon", "annual_report.announcement.description": "Descubra mais sobre seu engajamento no Mastodon ao longo do último ano.", "annual_report.announcement.title": "Chegou o Wrapstodon de {year}", - "annual_report.summary.archetype.booster": "Caçador legal", - "annual_report.summary.archetype.lurker": "O espreitador", - "annual_report.summary.archetype.oracle": "O oráculo", - "annual_report.summary.archetype.pollster": "O pesquisador", - "annual_report.summary.archetype.replier": "A borboleta social", - "annual_report.summary.followers.followers": "seguidores", - "annual_report.summary.followers.total": "{count} total", - "annual_report.summary.here_it_is": "Aqui está seu {year} em retrospectiva:", - "annual_report.summary.highlighted_post.by_favourites": "publicação mais favoritada", - "annual_report.summary.highlighted_post.by_reblogs": "publicação mais impulsionada", - "annual_report.summary.highlighted_post.by_replies": "publicação com mais respostas", - "annual_report.summary.highlighted_post.possessive": "{name}", + "annual_report.nav_item.badge": "Novo", + "annual_report.shared_page.donate": "Doe", + "annual_report.shared_page.footer": "Criado com {heart} pela equipe do Mastodon", + "annual_report.shared_page.footer_server_info": "{username} utiliza {domain}, uma das várias comunidades baseadas no Mastodon.", + "annual_report.summary.archetype.booster.desc_public": "{name} se manteve na caça por publicações para impulsionar, amplificando outros criadores com uma mira perfeita.", + "annual_report.summary.archetype.booster.desc_self": "Você se manteve na caça por publicações para impulsionar, amplificando outros criadores com uma mira perfeita.", + "annual_report.summary.archetype.booster.name": "O Arqueiro", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.desc_public": "Sabemos que {name} esteve por aí, em algum lugar, aproveitando o Mastodon do seu próprio jeito silencioso.", + "annual_report.summary.archetype.lurker.desc_self": "Sabemos que você esteve por aí, em algum lugar, aproveitando o Mastodon do seu próprio jeito silencioso.", + "annual_report.summary.archetype.lurker.name": "O Estoico", + "annual_report.summary.archetype.oracle.desc_public": "{name} criou mais publicações novas que respostas, mantendo o Mastodon fresco e em direção ao futuro.", + "annual_report.summary.archetype.oracle.desc_self": "Você criou mais publicações novas que respostas, mantendo o Mastodon fresco e em direção ao futuro.", + "annual_report.summary.archetype.oracle.name": "O Oráculo", + "annual_report.summary.archetype.pollster.desc_public": "{name} criou mais enquetes que quaisquer outros tipos de publicações, cultivando curiosidade no Mastodon.", + "annual_report.summary.archetype.pollster.desc_self": "Você criou mais enquetes que quaisquer outros tipos de publicações, cultivando curiosidade no Mastodon.", + "annual_report.summary.archetype.pollster.name": "O Questionador", + "annual_report.summary.archetype.replier.desc_public": "{name} frequentemente respondeu às publicações de outras pessoas, polinizando o Mastodon com novas discussões.", + "annual_report.summary.archetype.replier.desc_self": "Você frequentemente respondeu às publicações de outras pessoas, polinizando o Mastodon com novas discussões.", + "annual_report.summary.archetype.replier.name": "A Borboleta", + "annual_report.summary.archetype.reveal": "Revele meu arquétipo", + "annual_report.summary.archetype.reveal_description": "Obrigado por ser parte do Mastodon! Está na hora de descobrir qual arquétipo você incorporou em {year}.", + "annual_report.summary.archetype.title_public": "Arquétipo de {name}", + "annual_report.summary.archetype.title_self": "Seu arquétipo", + "annual_report.summary.close": "Fechar", + "annual_report.summary.copy_link": "Copiar link", + "annual_report.summary.followers.new_followers": "{count, plural, one {novo(a) seguidor(a)} other {novos seguidores}}", + "annual_report.summary.highlighted_post.boost_count": "Esta publicação foi impulsionada {count, plural, one {uma vez} other {# vezes}}.", + "annual_report.summary.highlighted_post.favourite_count": "Esta publicação foi favoritada {count, plural, one {uma vez} other {# vezes}}.", + "annual_report.summary.highlighted_post.reply_count": "Esta postagem recebeu {count, plural, one {uma resposta} other {# respostas}}.", + "annual_report.summary.highlighted_post.title": "Publicação mais popular", "annual_report.summary.most_used_app.most_used_app": "aplicativo mais usado", "annual_report.summary.most_used_hashtag.most_used_hashtag": "hashtag mais usada", - "annual_report.summary.most_used_hashtag.none": "Nenhuma", + "annual_report.summary.most_used_hashtag.used_count": "Você incluiu esta hashtag em {count, plural, one {uma publicação} other {# publicações}}.", + "annual_report.summary.most_used_hashtag.used_count_public": "{name} incluiu esta hashtag em {count, plural, one {uma publicação} other {# publicações}}.", "annual_report.summary.new_posts.new_posts": "novas publicações", "annual_report.summary.percentile.text": "Isso lhe coloca no topode usuários de {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "Não contaremos ao Bernie.", - "annual_report.summary.thanks": "Obrigada por fazer parte do Mastodon!", + "annual_report.summary.share_elsewhere": "Compartilhar em outro lugar", + "annual_report.summary.share_message": "Eu obtive o arquétipo {archetype}!", + "annual_report.summary.share_on_mastodon": "Compartilhar no Mastodon", "attachments_list.unprocessed": "(não processado)", "audio.hide": "Ocultar áudio", "block_modal.remote_users_caveat": "Pediremos ao servidor {domain} que respeite sua decisão. No entanto, a conformidade não é garantida, já que alguns servidores podem lidar com bloqueios de maneira diferente. As publicações públicas ainda podem estar visíveis para usuários não logados.", @@ -145,10 +168,10 @@ "block_modal.they_cant_see_posts": "Não poderá ver suas publicações e você não verá as dele/a.", "block_modal.they_will_know": "Poderá ver que você bloqueou.", "block_modal.title": "Bloquear usuário?", - "block_modal.you_wont_see_mentions": "Você não verá publicações que mencionem o usuário.", + "block_modal.you_wont_see_mentions": "Você não verá publicações que mencionem este usuário.", "boost_modal.combo": "Pressione {combo} para pular isto na próxima vez", "boost_modal.reblog": "Impulsionar a publicação?", - "boost_modal.undo_reblog": "Retirar o impulso do post?", + "boost_modal.undo_reblog": "Retirar o impulso da publicação?", "bundle_column_error.copy_stacktrace": "Copiar relatório do erro", "bundle_column_error.error.body": "A página solicitada não pôde ser renderizada. Pode ser devido a um erro no nosso código, ou um problema de compatibilidade do seu navegador.", "bundle_column_error.error.title": "Ah, não!", @@ -162,7 +185,7 @@ "bundle_modal_error.message": "Algo deu errado ao carregar esta tela.", "bundle_modal_error.retry": "Tente novamente", "carousel.current": "Slide {current, number} / {max, number}", - "carousel.slide": "Slide {current, number} of {max, number}", + "carousel.slide": "Slide {current, number} de {max, number}", "closed_registrations.other_server_instructions": "Como o Mastodon é descentralizado, você pode criar uma conta em outro servidor e ainda pode interagir com este.", "closed_registrations_modal.description": "Não é possível criar uma conta em {domain} no momento, mas atente que você não precisa de uma conta especificamente em {domain} para usar o Mastodon.", "closed_registrations_modal.find_another_server": "Encontrar outro servidor", @@ -179,8 +202,8 @@ "column.edit_list": "Editar lista", "column.favourites": "Favoritos", "column.firehose": "Feeds ao vivo", - "column.firehose_local": "Transmissão ao vivo deste servidor", - "column.firehose_singular": "Transmissão ao vivo", + "column.firehose_local": "Feed ao vivo deste servidor", + "column.firehose_singular": "Feed ao vivo", "column.follow_requests": "Seguidores pendentes", "column.home": "Página inicial", "column.list_members": "Gerenciar membros da lista", @@ -235,7 +258,7 @@ "confirmations.delete_list.title": "Excluir lista?", "confirmations.discard_draft.confirm": "Descartar e continuar", "confirmations.discard_draft.edit.cancel": "Continuar editando", - "confirmations.discard_draft.edit.message": "Continuar vai descartar quaisquer mudanças feitas à publicação sendo editada.", + "confirmations.discard_draft.edit.message": "Continuar descartará quaisquer mudanças feitas à publicação sendo editada.", "confirmations.discard_draft.edit.title": "Descartar mudanças na sua publicação?", "confirmations.discard_draft.post.cancel": "Continuar rascunho", "confirmations.discard_draft.post.message": "Continuar eliminará a publicação que está sendo elaborada no momento.", @@ -263,7 +286,7 @@ "confirmations.quiet_post_quote_info.message": "Ao citar uma publicação pública silenciosa, sua postagem será oculta das linhas de tempo em tendência.", "confirmations.quiet_post_quote_info.title": "Citando publicações públicas silenciadas", "confirmations.redraft.confirm": "Excluir e rascunhar", - "confirmations.redraft.message": "Você tem certeza de que quer apagar essa publicação e rascunhá-la? Favoritos e impulsos serão perdidos, e respostas à postagem original ficarão órfãs.", + "confirmations.redraft.message": "Você tem certeza de que quer apagar esta publicação e rascunhá-la? Favoritos e impulsos serão perdidos, e respostas à publicação original ficarão órfãs.", "confirmations.redraft.title": "Excluir e rascunhar publicação?", "confirmations.remove_from_followers.confirm": "Remover seguidor", "confirmations.remove_from_followers.message": "{name} vai parar de te seguir. Tem certeza de que deseja continuar?", @@ -304,8 +327,8 @@ "domain_block_modal.title": "Bloquear domínio?", "domain_block_modal.you_will_lose_num_followers": "Você perderá {followersCount, plural, one {{followersCountDisplay} seguidor} other {{followersCountDisplay} seguidores}} e {followingCount, plural, one {{followingCountDisplay} pessoa que você segue} other {{followingCountDisplay} pessoas que você segue}}.", "domain_block_modal.you_will_lose_relationships": "Você irá perder todos os seguidores e pessoas que você segue neste servidor.", - "domain_block_modal.you_wont_see_posts": "Você não verá postagens ou notificações de usuários neste servidor.", - "domain_pill.activitypub_lets_connect": "Ele permite que você se conecte e interaja com pessoas não apenas no Mastodon, mas também em diferentes aplicativos sociais.", + "domain_block_modal.you_wont_see_posts": "Você não verá publicações ou notificações de usuários neste servidor.", + "domain_pill.activitypub_lets_connect": "Permite que você se conecte e interaja com pessoas não apenas no Mastodon, mas também em diferentes aplicativos sociais.", "domain_pill.activitypub_like_language": "ActivityPub é como a linguagem que o Mastodon fala com outras redes sociais.", "domain_pill.server": "Servidor", "domain_pill.their_handle": "Identificador dele/a:", @@ -347,13 +370,13 @@ "empty_column.bookmarked_statuses": "Nada aqui. Quando você salvar um toot, ele aparecerá aqui.", "empty_column.community": "A linha local está vazia. Publique algo para começar!", "empty_column.direct": "Você ainda não tem mensagens privadas. Quando você enviar ou receber uma, será exibida aqui.", - "empty_column.disabled_feed": "Este feed foi desativado pelos administradores do servidor.", + "empty_column.disabled_feed": "Este feed foi desativado pelos administradores de seu servidor.", "empty_column.domain_blocks": "Nada aqui.", "empty_column.explore_statuses": "Nada está em alta no momento. Volte mais tarde!", "empty_column.favourited_statuses": "Você ainda não tem publicações favoritas. Quanto você marcar uma como favorita, ela aparecerá aqui.", "empty_column.favourites": "Ninguém marcou esta publicação como favorita até agora. Quando alguém o fizer, será listado aqui.", "empty_column.follow_requests": "Nada aqui. Quando você tiver seguidores pendentes, eles aparecerão aqui.", - "empty_column.followed_tags": "Você ainda não seguiu nenhuma hashtag. Quando seguir uma, elas serão exibidas aqui.", + "empty_column.followed_tags": "Você ainda não seguiu nenhuma hashtag. Quando seguir, elas serão exibidas aqui.", "empty_column.hashtag": "Nada aqui.", "empty_column.home": "Sua página inicial está vazia! Siga mais pessoas para começar: {suggestions}", "empty_column.list": "Nada aqui. Quando membros da lista tootarem, eles aparecerão aqui.", @@ -374,13 +397,13 @@ "explore.trending_statuses": "Publicações", "explore.trending_tags": "Hashtags", "featured_carousel.current": "Publicação {current, number} / {max, number}", - "featured_carousel.header": "{count, plural, one {Postagem fixada} other {Postagens fixadas}}", - "featured_carousel.slide": "Publicação {current, number} of {max, number}", + "featured_carousel.header": "{count, plural, one {Publicação fixada} other {Publicações fixadas}}", + "featured_carousel.slide": "Publicação {current, number} de {max, number}", "filter_modal.added.context_mismatch_explanation": "Esta categoria de filtro não se aplica ao contexto no qual você acessou esta publicação. Se quiser que a publicação seja filtrada nesse contexto também, você terá que editar o filtro.", "filter_modal.added.context_mismatch_title": "Incompatibilidade de contexto!", "filter_modal.added.expired_explanation": "Esta categoria de filtro expirou, você precisará alterar a data de expiração para aplicar.", "filter_modal.added.expired_title": "Filtro expirado!", - "filter_modal.added.review_and_configure": "Para revisar e configurar ainda mais esta categoria de filtro, vá até {settings_link}.", + "filter_modal.added.review_and_configure": "Para revisar e configurar ainda mais esta categoria de filtro, vá para {settings_link}.", "filter_modal.added.review_and_configure_title": "Configurações de filtro", "filter_modal.added.settings_link": "página de configurações", "filter_modal.added.short_explanation": "Esta publicação foi adicionada à seguinte categoria de filtro: {title}.", @@ -418,6 +441,8 @@ "follow_suggestions.who_to_follow": "Quem seguir", "followed_tags": "Hashtags seguidas", "footer.about": "Sobre", + "footer.about_mastodon": "Sobre o mastodon", + "footer.about_server": "Sobre {domain}", "footer.about_this_server": "Sobre", "footer.directory": "Diretório de perfis", "footer.get_app": "Baixe o app", @@ -612,7 +637,7 @@ "notification.admin.report_statuses": "{name} Reportou {target} para {category}", "notification.admin.report_statuses_other": "{name} denunciou {target}", "notification.admin.sign_up": "{name} se inscreveu", - "notification.admin.sign_up.name_and_others": "{name} e {count, plural, one {# other} other {# outros}}", + "notification.admin.sign_up.name_and_others": "{name} e {count, plural, one {# outro} other {# outros}} se inscreveram", "notification.annual_report.message": "O seu #Wrapstodon de {year} está esperando! Desvende seus destaques do ano e momentos memoráveis no Mastodon!", "notification.annual_report.view": "Ver #Wrapstodon", "notification.favourite": "{name} favoritou sua publicação", @@ -622,7 +647,7 @@ "notification.follow": "{name} te seguiu", "notification.follow.name_and_others": "{name} e {count, plural, one {# outro} other {# outros}} seguiram você", "notification.follow_request": "{name} quer te seguir", - "notification.follow_request.name_and_others": "{name} e {count, plural, one {# other} other {# outros}} pediu para seguir você", + "notification.follow_request.name_and_others": "{name} e {count, plural, one {# outro} other {# outros}} pediram para seguir você", "notification.label.mention": "Menção", "notification.label.private_mention": "Menção privada", "notification.label.private_reply": "Resposta privada", diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json index 671930e673564b..9676206e2ceafd 100644 --- a/app/javascript/mastodon/locales/pt-PT.json +++ b/app/javascript/mastodon/locales/pt-PT.json @@ -114,28 +114,51 @@ "alt_text_modal.done": "Concluído", "announcement.announcement": "Mensagem de manutenção", "annual_report.announcement.action_build": "Criar o meu Wrapstodon", + "annual_report.announcement.action_dismiss": "Não, obrigado", "annual_report.announcement.action_view": "Ver o meu Wrapstodon", "annual_report.announcement.description": "Descobre mais sobre o teu envolvimento com o Mastodon durante o último ano.", "annual_report.announcement.title": "Chegou o Wrapstodon {year}", - "annual_report.summary.archetype.booster": "O caçador de tendências", - "annual_report.summary.archetype.lurker": "O espreitador", - "annual_report.summary.archetype.oracle": "O oráculo", - "annual_report.summary.archetype.pollster": "O sondagens", - "annual_report.summary.archetype.replier": "A borboleta social", - "annual_report.summary.followers.followers": "seguidores", - "annual_report.summary.followers.total": "{count} no total", - "annual_report.summary.here_it_is": "Aqui está um resumo do ano {year}:", - "annual_report.summary.highlighted_post.by_favourites": "publicação mais favorita", - "annual_report.summary.highlighted_post.by_reblogs": "publicação mais partilhada", - "annual_report.summary.highlighted_post.by_replies": "publicação com o maior número de respostas", - "annual_report.summary.highlighted_post.possessive": "{name}", + "annual_report.nav_item.badge": "Novo", + "annual_report.shared_page.donate": "Doar", + "annual_report.shared_page.footer": "Gerado com {heart} pela equipa do Mastodon", + "annual_report.shared_page.footer_server_info": "{username} utiliza {domain}, uma das muitas comunidades baseadas no Mastodon.", + "annual_report.summary.archetype.booster.desc_public": "{name} permaneceu à procura de publicações para partilhar, promovendo outros criadores com uma precisão perfeita.", + "annual_report.summary.archetype.booster.desc_self": "Permaneceu à procura de publicações para partilhar, promovendo outros criadores com uma precisão perfeita.", + "annual_report.summary.archetype.booster.name": "O Arqueiro", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.desc_public": "Sabemos que {name} esteve por aí, algures, a apreciar o Mastodon na sua maneira discreta.", + "annual_report.summary.archetype.lurker.desc_self": "Sabemos que esteve por aí, algures, a apreciar o Mastodon na sua maneira discreta.", + "annual_report.summary.archetype.lurker.name": "O Estoico", + "annual_report.summary.archetype.oracle.desc_public": "{name} criou mais publicações novas do que respostas, mantendo o Mastodon atualizado e voltado para o futuro.", + "annual_report.summary.archetype.oracle.desc_self": "Criou mais publicações novas do que respostas, mantendo o Mastodon atualizado e voltado para o futuro.", + "annual_report.summary.archetype.oracle.name": "O Oráculo", + "annual_report.summary.archetype.pollster.desc_public": "{name} criou mais sondagens do que outros tipos de publicações, cultivando a curiosidade no Mastodon.", + "annual_report.summary.archetype.pollster.desc_self": "Criou mais sondagens do que outros tipos de publicações, cultivando a curiosidade no Mastodon.", + "annual_report.summary.archetype.pollster.name": "O Questionador", + "annual_report.summary.archetype.replier.desc_public": "{name} respondeu frequentemente às publicações de outras pessoas, polinizando o Mastodon com novas discussões.", + "annual_report.summary.archetype.replier.desc_self": "Respondeu frequentemente às publicações de outras pessoas, polinizando o Mastodon com novas discussões.", + "annual_report.summary.archetype.replier.name": "A Borboleta", + "annual_report.summary.archetype.reveal": "Revelar o meu arquétipo", + "annual_report.summary.archetype.reveal_description": "Obrigado por fazer parte do Mastodon! É hora de descobrir qual arquétipo você encarnou em {year}.", + "annual_report.summary.archetype.title_public": "Arquétipo de {name}", + "annual_report.summary.archetype.title_self": "O seu arquétipo", + "annual_report.summary.close": "Fechar", + "annual_report.summary.copy_link": "Copiar hiperligação", + "annual_report.summary.followers.new_followers": "{count, plural, one {novo seguidor} other {novos seguidores}}", + "annual_report.summary.highlighted_post.boost_count": "Esta publicação foi partilhada {count, plural, one {uma vez} other {# vezes}}.", + "annual_report.summary.highlighted_post.favourite_count": "Esta publicação foi colocada nos favoritos {count, plural, one {uma vez} other {# vezes}}.", + "annual_report.summary.highlighted_post.reply_count": "Esta publicação teve {count, plural, one {uma resposta} other {# respostas}}.", + "annual_report.summary.highlighted_post.title": "Publicação mais popular", "annual_report.summary.most_used_app.most_used_app": "aplicação mais utilizada", "annual_report.summary.most_used_hashtag.most_used_hashtag": "etiqueta mais utilizada", - "annual_report.summary.most_used_hashtag.none": "Nenhuma", + "annual_report.summary.most_used_hashtag.used_count": "Incluiu esta etiqueta {count, plural, one {numa publicação} other {em # publicações}}.", + "annual_report.summary.most_used_hashtag.used_count_public": "{name} incluiu esta etiqueta {count, plural, one {numa publicação} other {em # publicações}}.", "annual_report.summary.new_posts.new_posts": "novas publicações", "annual_report.summary.percentile.text": "Isso coloca-te no topodos utilizadores de {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "Este segredo fica entre nós.", - "annual_report.summary.thanks": "Obrigado por fazeres parte do Mastodon!", + "annual_report.summary.share_elsewhere": "Partilhar noutro local", + "annual_report.summary.share_message": "Eu obtive o arquétipo {archetype}!", + "annual_report.summary.share_on_mastodon": "Partilhar no Mastodon", "attachments_list.unprocessed": "(não processado)", "audio.hide": "Ocultar áudio", "block_modal.remote_users_caveat": "Vamos pedir ao servidor {domain} para respeitar a tua decisão. No entanto, não é garantido o seu cumprimento, uma vez que alguns servidores podem tratar os bloqueios de forma diferente. As publicações públicas podem continuar a ser visíveis para utilizadores não autenticados.", @@ -418,6 +441,8 @@ "follow_suggestions.who_to_follow": "Quem seguir", "followed_tags": "Etiquetas seguidas", "footer.about": "Sobre", + "footer.about_mastodon": "Sobre o Mastodon", + "footer.about_server": "Sobre {domain}", "footer.about_this_server": "Sobre", "footer.directory": "Diretório de perfis", "footer.get_app": "Obter a aplicação", @@ -1032,7 +1057,7 @@ "visibility_modal.helper.privacy_editing": "A visibilidade não pode ser alterada após a publicação ser publicada.", "visibility_modal.helper.privacy_private_self_quote": "As autocitações de publicações privadas não podem ser tornadas públicas.", "visibility_modal.helper.private_quoting": "As publicações apenas para seguidores criadas no Mastodon não podem ser citadas por outras pessoas.", - "visibility_modal.helper.unlisted_quoting": "Quando as pessoas o citarem, as publicações delas serão também ocultadas das tendências.", + "visibility_modal.helper.unlisted_quoting": "Quando as pessoas o citarem, as respetivas publicações também serão ocultadas dos destaques.", "visibility_modal.instructions": "Controle quem pode interagir com esta publicação. Também pode definir esta configuração para todas as publicações futuras, em Preferências > Padrões de publicação.", "visibility_modal.privacy_label": "Visibilidade", "visibility_modal.quote_followers": "Apenas seguidores", diff --git a/app/javascript/mastodon/locales/ro.json b/app/javascript/mastodon/locales/ro.json index 9bf8150ba04378..8f487696434cdf 100644 --- a/app/javascript/mastodon/locales/ro.json +++ b/app/javascript/mastodon/locales/ro.json @@ -82,19 +82,8 @@ "alert.unexpected.title": "Ups!", "alt_text_badge.title": "Text alternativ", "announcement.announcement": "Anunț", - "annual_report.summary.archetype.lurker": "Pânditorul", - "annual_report.summary.archetype.oracle": "Oracolul", - "annual_report.summary.archetype.pollster": "Sondatorul", - "annual_report.summary.archetype.replier": "Fluturele social", - "annual_report.summary.followers.followers": "urmăritori", - "annual_report.summary.followers.total": "{count} total", - "annual_report.summary.here_it_is": "Iată rezumatul dvs. al anului {year}:", - "annual_report.summary.highlighted_post.by_favourites": "cea mai favorizată postare", - "annual_report.summary.highlighted_post.by_reblogs": "cea mai boostată postare", - "annual_report.summary.highlighted_post.by_replies": "postarea cu cele mai multe răspunsuri", "annual_report.summary.most_used_app.most_used_app": "cea mai utilizată aplicație", "annual_report.summary.most_used_hashtag.most_used_hashtag": "cel mai utilizat hashtag", - "annual_report.summary.most_used_hashtag.none": "Niciunul", "annual_report.summary.new_posts.new_posts": "postări noi", "attachments_list.unprocessed": "(neprocesate)", "audio.hide": "Ascunde audio", diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json index 2461dcf7e4c21b..e6891eaced2a83 100644 --- a/app/javascript/mastodon/locales/ru.json +++ b/app/javascript/mastodon/locales/ru.json @@ -113,25 +113,25 @@ "alt_text_modal.describe_for_people_with_visual_impairments": "Добавьте описание для людей с нарушениями зрения…", "alt_text_modal.done": "Готово", "announcement.announcement": "Объявление", - "annual_report.summary.archetype.booster": "Репостер", - "annual_report.summary.archetype.lurker": "Молчун", - "annual_report.summary.archetype.oracle": "Гуру", - "annual_report.summary.archetype.pollster": "Опросчик", - "annual_report.summary.archetype.replier": "Душа компании", - "annual_report.summary.followers.followers": "подписчиков", - "annual_report.summary.followers.total": "{count} за всё время", - "annual_report.summary.here_it_is": "Вот ваши итоги {year} года:", - "annual_report.summary.highlighted_post.by_favourites": "пост с наибольшим количеством звёздочек", - "annual_report.summary.highlighted_post.by_reblogs": "самый популярный пост", - "annual_report.summary.highlighted_post.by_replies": "пост с наибольшим количеством ответов", - "annual_report.summary.highlighted_post.possessive": "{name}", + "annual_report.announcement.action_dismiss": "Нет, спасибо", + "annual_report.announcement.action_view": "Посмотреть мой Wrapstodon", + "annual_report.announcement.title": "Wrapstodon {year} уже здесь", + "annual_report.nav_item.badge": "Новый", + "annual_report.shared_page.donate": "Пожертвовать", + "annual_report.shared_page.footer": "Сгенерировано с {heart} командой Mastodon", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.reveal": "Показать мой архетип", + "annual_report.summary.archetype.title_public": "Архетип {name}", + "annual_report.summary.archetype.title_self": "Ваш архетип", + "annual_report.summary.close": "Закрыть", + "annual_report.summary.copy_link": "Скопировать ссылку", "annual_report.summary.most_used_app.most_used_app": "наиболее часто используемое приложение", "annual_report.summary.most_used_hashtag.most_used_hashtag": "наиболее часто используемый хештег", - "annual_report.summary.most_used_hashtag.none": "Нет", "annual_report.summary.new_posts.new_posts": "новых постов", "annual_report.summary.percentile.text": "Всё это помещает вас в топпользователей {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "Роскомнадзор об этом не узнает.", - "annual_report.summary.thanks": "Спасибо за то, что были вместе с Mastodon!", + "annual_report.summary.share_elsewhere": "Поделиться на других", + "annual_report.summary.share_on_mastodon": "Поделиться в Mastodon", "attachments_list.unprocessed": "(не обработан)", "audio.hide": "Скрыть аудио", "block_modal.remote_users_caveat": "Мы попросим сервер {domain} уважать ваше решение, однако нельзя гарантировать, что он будет соблюдать блокировку, поскольку некоторые серверы могут по-разному обрабатывать запросы. Публичные посты по-прежнему могут быть видны неавторизованным пользователям.", diff --git a/app/javascript/mastodon/locales/sc.json b/app/javascript/mastodon/locales/sc.json index 8b4f4cc3d5f27a..5ba3d1cd968268 100644 --- a/app/javascript/mastodon/locales/sc.json +++ b/app/javascript/mastodon/locales/sc.json @@ -104,16 +104,10 @@ "alt_text_modal.change_thumbnail": "Càmbia sa miniadura", "alt_text_modal.done": "Fatu", "announcement.announcement": "Annùntziu", - "annual_report.summary.archetype.booster": "Semper a s'ùrtima", - "annual_report.summary.followers.followers": "sighiduras", - "annual_report.summary.followers.total": "{count} totale", - "annual_report.summary.highlighted_post.possessive": "de {name}", "annual_report.summary.most_used_app.most_used_app": "aplicatzione prus impreada", "annual_report.summary.most_used_hashtag.most_used_hashtag": "eticheta prus impreada", - "annual_report.summary.most_used_hashtag.none": "Peruna", "annual_report.summary.new_posts.new_posts": "publicatziones noas", "annual_report.summary.percentile.we_wont_tell_bernie": "No dd'amus a nàrrere a Bernie.", - "annual_report.summary.thanks": "Gràtzias de èssere parte de Mastodon!", "attachments_list.unprocessed": "(non protzessadu)", "audio.hide": "Cua s'àudio", "block_modal.remote_users_caveat": "Amus a pedire a su serbidore {domain} de rispetare sa detzisione tua. Nointames custu, su rispetu no est garantidu ca unos cantos serbidores diant pòdere gestire is blocos de manera diferente. Is publicatzione pùblicas diant pòdere ancora èssere visìbiles a is utentes chi no ant fatu s'atzessu.", diff --git a/app/javascript/mastodon/locales/si.json b/app/javascript/mastodon/locales/si.json index 8b6ab5688326c0..7b7ec1f8a2bde5 100644 --- a/app/javascript/mastodon/locales/si.json +++ b/app/javascript/mastodon/locales/si.json @@ -105,25 +105,11 @@ "alt_text_modal.describe_for_people_with_visual_impairments": "දෘශ්‍යාබාධිත පුද්ගලයින් සඳහා මෙය විස්තර කරන්න…", "alt_text_modal.done": "කළා", "announcement.announcement": "නිවේදනය", - "annual_report.summary.archetype.booster": "සිසිල් දඩයක්කාරයා", - "annual_report.summary.archetype.lurker": "සැඟවී සිටින්නා", - "annual_report.summary.archetype.oracle": "ඔරකල්", - "annual_report.summary.archetype.pollster": "ඡන්ද විමසන්නා", - "annual_report.summary.archetype.replier": "සමාජ සමනලයා", - "annual_report.summary.followers.followers": "අනුගාමිකයින්", - "annual_report.summary.followers.total": "මුළු {count}", - "annual_report.summary.here_it_is": "මෙන්න ඔබේ {year} සමාලෝචනය:", - "annual_report.summary.highlighted_post.by_favourites": "වඩාත්ම කැමති පළ කිරීම", - "annual_report.summary.highlighted_post.by_reblogs": "වඩාත්ම ප්‍රවර්ධනය කරන ලද පළ කිරීම", - "annual_report.summary.highlighted_post.by_replies": "වැඩිම පිළිතුරු සහිත පළ කිරීම", - "annual_report.summary.highlighted_post.possessive": "{name}ගේ", "annual_report.summary.most_used_app.most_used_app": "වැඩිපුරම භාවිතා කරන යෙදුම", "annual_report.summary.most_used_hashtag.most_used_hashtag": "වැඩිපුරම භාවිතා කරන ලද හැෂ් ටැගය", - "annual_report.summary.most_used_hashtag.none": "කිසිවක් නැත", "annual_report.summary.new_posts.new_posts": "නව පළ කිරීම්", "annual_report.summary.percentile.text": "එය ඔබව {domain} පරිශීලකයින්ගෙන් ඉහළමඅතරට ගෙන එයි.", "annual_report.summary.percentile.we_wont_tell_bernie": "අපි බර්නිට කියන්නේ නැහැ.", - "annual_report.summary.thanks": "මැස්ටෝඩන් හි කොටසක් වීම ගැන ස්තූතියි!", "attachments_list.unprocessed": "(සකසා නැති)", "audio.hide": "හඬපටය සඟවන්න", "block_modal.remote_users_caveat": "ඔබගේ තීරණයට ගරු කරන ලෙස අපි සේවාදායකයාගෙන් {domain} ඉල්ලා සිටිමු. කෙසේ වෙතත්, සමහර සේවාදායකයන් අවහිර කිරීම් වෙනස් ලෙස හසුරුවන බැවින් අනුකූලතාව සහතික නොවේ. පොදු පළ කිරීම් තවමත් ලොග් වී නොමැති පරිශීලකයින්ට දෘශ්‍යමාන විය හැකිය.", diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json index c440c78623792f..87282c41df27b9 100644 --- a/app/javascript/mastodon/locales/sk.json +++ b/app/javascript/mastodon/locales/sk.json @@ -113,25 +113,11 @@ "alt_text_modal.describe_for_people_with_visual_impairments": "Opíšte obsah pre ľudí so zrakovým postihnutím…", "alt_text_modal.done": "Hotovo", "announcement.announcement": "Oznámenie", - "annual_report.summary.archetype.booster": "Zdieľač*ka", - "annual_report.summary.archetype.lurker": "Sliedič*ka", - "annual_report.summary.archetype.oracle": "Divoká karta", - "annual_report.summary.archetype.pollster": "Anketár*ka", - "annual_report.summary.archetype.replier": "Hlasná trúba", - "annual_report.summary.followers.followers": "sledovatelia", - "annual_report.summary.followers.total": "{count} celkovo", - "annual_report.summary.here_it_is": "Tu je tvoja {year} v zhrnutí:", - "annual_report.summary.highlighted_post.by_favourites": "najviac obľúbený príspevok", - "annual_report.summary.highlighted_post.by_reblogs": "najviac zdieľaný príspevok", - "annual_report.summary.highlighted_post.by_replies": "príspevok s najviac odpoveďami", - "annual_report.summary.highlighted_post.possessive": "{name}", "annual_report.summary.most_used_app.most_used_app": "najviac používaná aplikácia", "annual_report.summary.most_used_hashtag.most_used_hashtag": "najviac užívaný hashtag", - "annual_report.summary.most_used_hashtag.none": "Žiaden", "annual_report.summary.new_posts.new_posts": "nové príspevky", "annual_report.summary.percentile.text": "Takže patríte do topúčtov na {domain}", "annual_report.summary.percentile.we_wont_tell_bernie": "Nepovieme Berniemu.", - "annual_report.summary.thanks": "Vďaka, že si súčasťou Mastodonu!", "attachments_list.unprocessed": "(nespracované)", "audio.hide": "Skryť zvuk", "block_modal.remote_users_caveat": "Požiadame server {domain} o rešpektovanie vášho rozhodnutia. Nevieme to však zaručiť, keďže niektoré servery pristupujú k blokovaniu inak. Verejné príspevky sa stále môžu zobrazovať neprihláseným ľuďom.", diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json index 7f7d8bd9910658..30c72c87f0b847 100644 --- a/app/javascript/mastodon/locales/sl.json +++ b/app/javascript/mastodon/locales/sl.json @@ -103,25 +103,11 @@ "alt_text_modal.describe_for_people_with_visual_impairments": "Podaj opis za slabovidne ...", "alt_text_modal.done": "Opravljeno", "announcement.announcement": "Oznanilo", - "annual_report.summary.archetype.booster": "Lovec na trende", - "annual_report.summary.archetype.lurker": "Tiholazec", - "annual_report.summary.archetype.oracle": "Orakelj", - "annual_report.summary.archetype.pollster": "Anketar", - "annual_report.summary.archetype.replier": "Priljudnež", - "annual_report.summary.followers.followers": "sledilcev", - "annual_report.summary.followers.total": "skupaj {count}", - "annual_report.summary.here_it_is": "Tule je povzetek vašega leta {year}:", - "annual_report.summary.highlighted_post.by_favourites": "- najpriljubljenejša objava", - "annual_report.summary.highlighted_post.by_reblogs": "- največkrat izpostavljena objava", - "annual_report.summary.highlighted_post.by_replies": "- objava z največ odgovori", - "annual_report.summary.highlighted_post.possessive": "{name}", "annual_report.summary.most_used_app.most_used_app": "najpogosteje uporabljena aplikacija", "annual_report.summary.most_used_hashtag.most_used_hashtag": "največkrat uporabljen ključnik", - "annual_report.summary.most_used_hashtag.none": "Brez", "annual_report.summary.new_posts.new_posts": "nove objave", "annual_report.summary.percentile.text": "S tem ste se uvrstili med zgornjih uporabnikov domene {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "Živi duši ne bomo povedali.", - "annual_report.summary.thanks": "Hvala, ker ste del Mastodona!", "attachments_list.unprocessed": "(neobdelano)", "audio.hide": "Skrij zvok", "block_modal.remote_users_caveat": "Strežnik {domain} bomo pozvali, naj spoštuje vašo odločitev. Kljub temu pa ni gotovo, da bo strežnik prošnjo upošteval, saj nekateri strežniki blokiranja obravnavajo drugače. Javne objave bodo morda še vedno vidne neprijavljenim uporabnikom.", diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json index af268b1e494de0..0c8e1fc071f17c 100644 --- a/app/javascript/mastodon/locales/sq.json +++ b/app/javascript/mastodon/locales/sq.json @@ -113,21 +113,44 @@ "alt_text_modal.describe_for_people_with_visual_impairments": "Përshkruajeni këtë për persona me mangësi shikimi…", "alt_text_modal.done": "U bë", "announcement.announcement": "Lajmërim", - "annual_report.summary.archetype.oracle": "Orakulli", - "annual_report.summary.followers.followers": "ndjekës", - "annual_report.summary.followers.total": "{count} gjithsej", - "annual_report.summary.here_it_is": "Ja {year} juaj e shqyrtuar:", - "annual_report.summary.highlighted_post.by_favourites": "potimi më i parapëlqyer", - "annual_report.summary.highlighted_post.by_reblogs": "postimi me më shumë përforcime", - "annual_report.summary.highlighted_post.by_replies": "postimi me më tepër përgjigje", - "annual_report.summary.highlighted_post.possessive": "nga {name}", + "annual_report.announcement.action_dismiss": "Jo, faleminderit", + "annual_report.nav_item.badge": "E re", + "annual_report.shared_page.donate": "Dhuroni", + "annual_report.shared_page.footer": "Hartuar me {heart} nga ekipi i Mastodon-it", + "annual_report.shared_page.footer_server_info": "{username} përdor {domain}, një nga bashkësitë e shumta të bazuara në Mastodon.", + "annual_report.summary.archetype.booster.name": "Harkëtari", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.desc_public": "E dimë se {name} qe vërdallë, diku, duke shijuar Mastodon-in në mënyrën e vet të qetë.", + "annual_report.summary.archetype.lurker.desc_self": "E dimë se qetë vërdallë, diku, duke shijuar Mastodon-in në mënyrën tuaj të qetë.", + "annual_report.summary.archetype.lurker.name": "Stoiku", + "annual_report.summary.archetype.oracle.desc_public": "{name} krijoi postime të reja, më shumë se përgjigje, duke e mbajtur Mastodon-in të freskët dhe me sytë nga ardhmja.", + "annual_report.summary.archetype.oracle.desc_self": "Krijuat postime të reja, më shumë se përgjigje, duke e mbajtur Mastodon-in të freskët dhe me sytë nga e ardhmja.", + "annual_report.summary.archetype.oracle.name": "Orakulli", + "annual_report.summary.archetype.pollster.desc_public": "{name} krijoi më tepër pyetësorë se sa lloje të tjera postimesh, duke kultivuar kureshtjen në Mastodon.", + "annual_report.summary.archetype.pollster.desc_self": "Krijuat më tepër pyetësorë, se sa lloje të tjera postimesh, duke kultivuar kureshtjen në Mastodon.", + "annual_report.summary.archetype.replier.desc_public": "{name} u përgjigj shpesh te postime të njerëzve të tjerë, duke pjalmuar Mastodon-in me diskutime të reja.", + "annual_report.summary.archetype.replier.desc_self": "U përgjigjët shpesh te postime të njerëzve të tjerë, duke pjalmuar Mastodon-in me diskutime të reja.", + "annual_report.summary.archetype.replier.name": "Flutura", + "annual_report.summary.archetype.reveal": "Zbulo me se ngjaj", + "annual_report.summary.archetype.reveal_description": "Faleminderit për qenien pjesë e Mastodon-it! Koha për të mësuar cilin model trupëzuat gjatë {year}.", + "annual_report.summary.archetype.title_public": "Modeli për {name}", + "annual_report.summary.archetype.title_self": "Modeli juaj", + "annual_report.summary.close": "Mbylle", + "annual_report.summary.copy_link": "Kopjoji lidhjen", + "annual_report.summary.followers.new_followers": "{count, plural, one {ndjekës i ri} other {ndjekës të rinj}}", + "annual_report.summary.highlighted_post.boost_count": "Ky postim qe përforcuar {count, plural, one {një herë} other {# herë}}.", + "annual_report.summary.highlighted_post.favourite_count": "Ky postim u vu si i parapëlqyer {count, plural, one {një herë} other {# herë}}.", + "annual_report.summary.highlighted_post.reply_count": "Ky postim pati {count, plural, one {një përgjigje} other {# përgjigje}}.", + "annual_report.summary.highlighted_post.title": "Postimi më popullor", "annual_report.summary.most_used_app.most_used_app": "aplikacioni më i përdorur", "annual_report.summary.most_used_hashtag.most_used_hashtag": "hashtag-u më i përdorur", - "annual_report.summary.most_used_hashtag.none": "Asnjë", + "annual_report.summary.most_used_hashtag.used_count": "Këtë hashtag e përfshitë në {count, plural, one {një postim} other {# postime}}.", + "annual_report.summary.most_used_hashtag.used_count_public": "{name} përfshiu këtë hashtag në {count, plural, one {një postim} other {# postime}}.", "annual_report.summary.new_posts.new_posts": "postime të reja", "annual_report.summary.percentile.text": "Kjo ju vendos te kryesues të përdoruesve të {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "Nuk do t’ia themi Bernit.", - "annual_report.summary.thanks": "Faleminderit që jeni pjesë e Mastodon-it!", + "annual_report.summary.share_elsewhere": "Ndajeni me të tjerë gjetkë", + "annual_report.summary.share_on_mastodon": "Ndajeni në Mastodon me të tjerë", "attachments_list.unprocessed": "(e papërpunuar)", "audio.hide": "Fshihe audion", "block_modal.remote_users_caveat": "Do t’i kërkojmë shërbyesit {domain} të respektojë vendimin tuaj. Por, pajtimi s’është i garantuar, ngaqë disa shërbyes mund t’i trajtojnë ndryshe bllokimet. Psotimet publike mundet të jenë ende të dukshme për përdorues pa bërë hyrje në llogari.", @@ -410,6 +433,8 @@ "follow_suggestions.who_to_follow": "Cilët të ndiqen", "followed_tags": "Hashtag-ë të ndjekur", "footer.about": "Mbi", + "footer.about_mastodon": "Mbi Mastodon-in", + "footer.about_server": "Mbi {domain}", "footer.about_this_server": "Mbi", "footer.directory": "Drejtori profilesh", "footer.get_app": "Merreni aplikacionin", diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json index 368498b1ce8c79..bbd3b68a7c1a29 100644 --- a/app/javascript/mastodon/locales/sv.json +++ b/app/javascript/mastodon/locales/sv.json @@ -113,25 +113,17 @@ "alt_text_modal.describe_for_people_with_visual_impairments": "Beskriv detta för personer med synnedsättning…", "alt_text_modal.done": "Klar", "announcement.announcement": "Kungörelse", - "annual_report.summary.archetype.booster": "Häftighetsjägaren", - "annual_report.summary.archetype.lurker": "Smygaren", - "annual_report.summary.archetype.oracle": "Oraklet", - "annual_report.summary.archetype.pollster": "Frågaren", - "annual_report.summary.archetype.replier": "Den sociala fjärilen", - "annual_report.summary.followers.followers": "följare", - "annual_report.summary.followers.total": "{count} totalt", - "annual_report.summary.here_it_is": "Här är en tillbakablick på ditt {year}:", - "annual_report.summary.highlighted_post.by_favourites": "mest favoritmarkerat inlägg", - "annual_report.summary.highlighted_post.by_reblogs": "mest boostat inlägg", - "annual_report.summary.highlighted_post.by_replies": "inlägg med flest svar", - "annual_report.summary.highlighted_post.possessive": "{name}s", + "annual_report.announcement.action_dismiss": "Nej tack", + "annual_report.nav_item.badge": "Ny", + "annual_report.shared_page.donate": "Donera", + "annual_report.summary.copy_link": "Kopiera länk", "annual_report.summary.most_used_app.most_used_app": "mest använda app", "annual_report.summary.most_used_hashtag.most_used_hashtag": "mest använda hashtag", - "annual_report.summary.most_used_hashtag.none": "Inga", "annual_report.summary.new_posts.new_posts": "nya inlägg", "annual_report.summary.percentile.text": "Det placerar dig i toppbland {domain} användare.", "annual_report.summary.percentile.we_wont_tell_bernie": "Vi berättar inte för Bernie.", - "annual_report.summary.thanks": "Tack för att du är en del av Mastodon!", + "annual_report.summary.share_elsewhere": "Dela någon annanstans", + "annual_report.summary.share_on_mastodon": "Dela på Mastodon", "attachments_list.unprocessed": "(obehandlad)", "audio.hide": "Dölj audio", "block_modal.remote_users_caveat": "Vi kommer att be servern {domain} att respektera ditt beslut. Dock garanteras inte efterlevnad eftersom vissa servrar kan hantera blockeringar på olika sätt. Offentliga inlägg kan fortfarande vara synliga för icke-inloggade användare.", @@ -414,6 +406,8 @@ "follow_suggestions.who_to_follow": "Rekommenderade profiler", "followed_tags": "Följda hashtags", "footer.about": "Om", + "footer.about_mastodon": "Om Mastodon", + "footer.about_server": "Om {domain}", "footer.about_this_server": "Om", "footer.directory": "Profilkatalog", "footer.get_app": "Skaffa appen", diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json index 1ba048635731bf..eb2a814d610e23 100644 --- a/app/javascript/mastodon/locales/th.json +++ b/app/javascript/mastodon/locales/th.json @@ -110,23 +110,10 @@ "alt_text_modal.describe_for_people_with_visual_impairments": "อธิบายสิ่งนี้สำหรับผู้คนที่มีความบกพร่องทางการมองเห็น…", "alt_text_modal.done": "เสร็จสิ้น", "announcement.announcement": "ประกาศ", - "annual_report.summary.archetype.booster": "ผู้ล่าความเจ๋ง", - "annual_report.summary.archetype.lurker": "ผู้ซุ่มอ่านข่าว", - "annual_report.summary.archetype.oracle": "ผู้ให้คำปรึกษา", - "annual_report.summary.archetype.pollster": "ผู้สำรวจความคิดเห็น", - "annual_report.summary.archetype.replier": "ผู้ชอบเข้าสังคม", - "annual_report.summary.followers.followers": "ผู้ติดตาม", - "annual_report.summary.followers.total": "รวม {count}", - "annual_report.summary.highlighted_post.by_favourites": "โพสต์ที่ได้รับการชื่นชอบมากที่สุด", - "annual_report.summary.highlighted_post.by_reblogs": "โพสต์ที่ได้รับการดันมากที่สุด", - "annual_report.summary.highlighted_post.by_replies": "โพสต์ที่มีการตอบกลับมากที่สุด", - "annual_report.summary.highlighted_post.possessive": "{name}", "annual_report.summary.most_used_app.most_used_app": "แอปที่ใช้มากที่สุด", "annual_report.summary.most_used_hashtag.most_used_hashtag": "แฮชแท็กที่ใช้มากที่สุด", - "annual_report.summary.most_used_hashtag.none": "ไม่มี", "annual_report.summary.new_posts.new_posts": "โพสต์ใหม่", "annual_report.summary.percentile.we_wont_tell_bernie": "เราจะไม่บอก Bernie", - "annual_report.summary.thanks": "ขอบคุณสำหรับการเป็นส่วนหนึ่งของ Mastodon!", "attachments_list.unprocessed": "(ยังไม่ได้ประมวลผล)", "audio.hide": "ซ่อนเสียง", "block_modal.remote_users_caveat": "เราจะขอให้เซิร์ฟเวอร์ {domain} เคารพการตัดสินใจของคุณ อย่างไรก็ตาม ไม่รับประกันการปฏิบัติตามข้อกำหนดเนื่องจากเซิร์ฟเวอร์บางแห่งอาจจัดการการปิดกั้นแตกต่างกัน โพสต์สาธารณะอาจยังคงปรากฏแก่ผู้ใช้ที่ไม่ได้เข้าสู่ระบบ", diff --git a/app/javascript/mastodon/locales/tok.json b/app/javascript/mastodon/locales/tok.json index 7ed0b16de13156..8faa4fa17f7dec 100644 --- a/app/javascript/mastodon/locales/tok.json +++ b/app/javascript/mastodon/locales/tok.json @@ -112,25 +112,11 @@ "alt_text_modal.describe_for_people_with_visual_impairments": "jan li ken ala lukin la o pana e toki pi sona lukin…", "alt_text_modal.done": "o pana", "announcement.announcement": "toki suli", - "annual_report.summary.archetype.booster": "jan ni li alasa e pona", - "annual_report.summary.archetype.lurker": "jan ni li lukin taso", - "annual_report.summary.archetype.oracle": "jan ni li sona suli", - "annual_report.summary.archetype.pollster": "jan ni li wile sona e pilin jan", - "annual_report.summary.archetype.replier": "jan ni li toki tawa jan mute", - "annual_report.summary.followers.followers": "jan kute sina", - "annual_report.summary.followers.total": "ale la {count}", - "annual_report.summary.here_it_is": "toki lili la tenpo sike nanpa {year} li sama ni tawa sina:", - "annual_report.summary.highlighted_post.by_favourites": "toki ni li pona suli tawa jan", - "annual_report.summary.highlighted_post.by_reblogs": "jan ante li pana suli e toki ni", - "annual_report.summary.highlighted_post.by_replies": "la jan ante li toki suli tan toki ni", - "annual_report.summary.highlighted_post.possessive": "{name}", "annual_report.summary.most_used_app.most_used_app": "ilo pi kepeken suli", "annual_report.summary.most_used_hashtag.most_used_hashtag": "kulupu toki pi kepeken suli", - "annual_report.summary.most_used_hashtag.none": "ala", "annual_report.summary.new_posts.new_posts": "toki suli sin", "annual_report.summary.percentile.text": "ni la sina nanpa sewipi jan ale lon {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "sona ni li len tawa ale.", - "annual_report.summary.thanks": "sina lon kulupu Mastodon la sina pona a!", "attachments_list.unprocessed": "(nasin open)", "audio.hide": "o len e kalama", "block_modal.remote_users_caveat": "mi pana e wile sina tawa ma {domain}. taso ma li ken kepeken nasin ante la pakala li ken. jan li awen ken lukin e toki kepeken sijelo ala.", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index 54a993acf6234f..98291db5c0d051 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -114,29 +114,50 @@ "alt_text_modal.done": "Tamamlandı", "announcement.announcement": "Duyuru", "annual_report.announcement.action_build": "Wrapstodon'umu Oluştur", + "annual_report.announcement.action_dismiss": "Hayır teşekkürler", "annual_report.announcement.action_view": "Wrapstodon'umu Görüntüle", "annual_report.announcement.description": "Mastodon'daki geçen yıldaki etkileşimleriniz hakkında daha fazlasını öğrenin.", "annual_report.announcement.title": "{year} Wrapstodon'u yayında", - "annual_report.summary.archetype.booster": "Trend takipçisi", - "annual_report.summary.archetype.lurker": "Gizli meraklı", - "annual_report.summary.archetype.oracle": "Kahin", - "annual_report.summary.archetype.pollster": "Anketör", - "annual_report.summary.archetype.replier": "Sosyal kelebek", - "annual_report.summary.followers.followers": "takipçiler", - "annual_report.summary.followers.total": "{count} toplam", - "annual_report.summary.here_it_is": "İşte {year} yılı değerlendirmeniz:", - "annual_report.summary.highlighted_post.by_favourites": "en çok beğenilen gönderi", - "annual_report.summary.highlighted_post.by_reblogs": "en çok paylaşılan gönderi", - "annual_report.summary.highlighted_post.by_replies": "en çok yanıt alan gönderi", - "annual_report.summary.highlighted_post.possessive": "{name}", + "annual_report.shared_page.donate": "Bağış Yap", + "annual_report.shared_page.footer": "Mastodon ekibi tarafından {heart} ile oluşturulmuştur", + "annual_report.shared_page.footer_server_info": "{username}, Mastodon tarafından desteklenen birçok topluluktan biri olan {domain} kullanıyor.", + "annual_report.summary.archetype.booster.desc_public": "{name} mükemmel bir hedefle diğer içerik üreticilerini desteklemek için paylaşımları yeniden yayınlamaya devam etti.", + "annual_report.summary.archetype.booster.desc_self": "Mükemmel bir hedefle diğer içerik üreticilerini desteklemek için paylaşımları yeniden yayınlamaya devam ettin.", + "annual_report.summary.archetype.booster.name": "Okçu", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.desc_public": "{name} orada, bir yerlerde, kendi sessiz tarzıyla Mastodon'un tadını çıkarıyor, biliyoruz.", + "annual_report.summary.archetype.lurker.desc_self": "Orada, bir yerlerde, kendi sessiz tarzınla Mastodon'un tadını çıkarıyorsun, biliyoruz.", + "annual_report.summary.archetype.lurker.name": "Stoacı", + "annual_report.summary.archetype.oracle.desc_public": "{name} yanıtlardan daha çok yeni gönderi oluşturarak Mastodon'u taze ve geleceğe dönük tuttu.", + "annual_report.summary.archetype.oracle.desc_self": "Yanıtlardan daha çok yeni gönderi oluşturarak Mastodon'u taze ve geleceğe dönük tuttun.", + "annual_report.summary.archetype.oracle.name": "Kahin", + "annual_report.summary.archetype.pollster.desc_public": "{name} diğer gönderi türlerinden çok anket oluşturarak Mastodon'da merak uyandırdı.", + "annual_report.summary.archetype.pollster.desc_self": "Diğer gönderi türlerinden çok anket oluşturarak Mastodon'da merak uyandırdın.", + "annual_report.summary.archetype.pollster.name": "Meraklı", + "annual_report.summary.archetype.replier.desc_public": "{name} sık sık diğer kullanıcıların gönderilerine yanıt vererek Mastodon'a yeni tartışmalar kazandırdı.", + "annual_report.summary.archetype.replier.desc_self": "Sık sık diğer kullanıcıların gönderilerine yanıt vererek Mastodon'a yeni tartışmalar kazandırdın.", + "annual_report.summary.archetype.replier.name": "Kelebek", + "annual_report.summary.archetype.reveal": "Arketipimi Göster", + "annual_report.summary.archetype.reveal_description": "Mastodon'un bir parçası olduğun için teşekkürler! {year} yılında hangi arketipi temsil ettiğini öğrenme zamanı geldi.", + "annual_report.summary.archetype.title_public": "{name} kullanıcısının arketipi", + "annual_report.summary.archetype.title_self": "Arketipin", + "annual_report.summary.close": "Kapat", + "annual_report.summary.copy_link": "Bağlantıyı kopyala", + "annual_report.summary.followers.new_followers": "{count, plural, one {1 yeni takipçi} other {# yeni takipçi}}", + "annual_report.summary.highlighted_post.boost_count": "Bu gönderi {count, plural, one {1 kez} other {# kez}} yeniden paylaşıldı.", + "annual_report.summary.highlighted_post.favourite_count": "Bu gönderi {count, plural, one {1 kez} other {# kez}} beğenildi.", + "annual_report.summary.highlighted_post.reply_count": "Bu gönderi {count, plural, one {1 yanıt} other {# yanıt}} aldı.", + "annual_report.summary.highlighted_post.title": "En popüler gönderi", "annual_report.summary.most_used_app.most_used_app": "en çok kullanılan uygulama", "annual_report.summary.most_used_hashtag.most_used_hashtag": "en çok kullanılan etiket", - "annual_report.summary.most_used_hashtag.none": "Yok", + "annual_report.summary.most_used_hashtag.used_count": "Bu etiketi {count, plural, one {1 gönderi} other {# gönderi}}de kullandınız.", + "annual_report.summary.most_used_hashtag.used_count_public": "{name} bu etiketi {count, plural, one {1 gönderi} other {# gönderi}}de kullandı.", "annual_report.summary.new_posts.new_posts": "yeni gönderiler", "annual_report.summary.percentile.text": "{domain} kullanıcılarınınüst dilimindesiniz", "annual_report.summary.percentile.we_wont_tell_bernie": "Bernie'ye söylemeyiz.", + "annual_report.summary.share_elsewhere": "Başka yerde paylaş", "annual_report.summary.share_message": "{archetype} arketipindeyim!", - "annual_report.summary.thanks": "Mastodon'un bir parçası olduğunuz için teşekkürler!", + "annual_report.summary.share_on_mastodon": "Mastodon'da Paylaş", "attachments_list.unprocessed": "(işlenmemiş)", "audio.hide": "Sesi gizle", "block_modal.remote_users_caveat": "{domain} sunucusundan kararınıza saygı duymasını isteyeceğiz. Ancak, Uymaları garanti değildir çünkü bazı sunucular engellemeyi farklı şekilde yapıyorlar. Herkese açık gönderiler giriş yapmamış kullanıcılara görüntülenmeye devam edebilir.", @@ -419,6 +440,8 @@ "follow_suggestions.who_to_follow": "Takip edebileceklerin", "followed_tags": "Takip edilen etiketler", "footer.about": "Hakkında", + "footer.about_mastodon": "Mastodon Hakkında", + "footer.about_server": "{domain} Hakkında", "footer.about_this_server": "Hakkında", "footer.directory": "Profil dizini", "footer.get_app": "Uygulamayı indir", diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json index dc48e9dafd0b8b..38ec3d3e1c21f8 100644 --- a/app/javascript/mastodon/locales/uk.json +++ b/app/javascript/mastodon/locales/uk.json @@ -110,25 +110,11 @@ "alt_text_modal.describe_for_people_with_visual_impairments": "Опишіть цю ідею для людей із порушеннями зору…", "alt_text_modal.done": "Готово", "announcement.announcement": "Оголошення", - "annual_report.summary.archetype.booster": "Мисливець на дописи", - "annual_report.summary.archetype.lurker": "Причаєнець", - "annual_report.summary.archetype.oracle": "Оракул", - "annual_report.summary.archetype.pollster": "Опитувач", - "annual_report.summary.archetype.replier": "Душа компанії", - "annual_report.summary.followers.followers": "підписники", - "annual_report.summary.followers.total": "Загалом {count}", - "annual_report.summary.here_it_is": "Ось ваші підсумки {year} року:", - "annual_report.summary.highlighted_post.by_favourites": "найуподобаніші дописи", - "annual_report.summary.highlighted_post.by_reblogs": "найпоширюваніші дописи", - "annual_report.summary.highlighted_post.by_replies": "найкоментованіші дописи", - "annual_report.summary.highlighted_post.possessive": "{name}", "annual_report.summary.most_used_app.most_used_app": "найчастіше використовуваний застосунок", "annual_report.summary.most_used_hashtag.most_used_hashtag": "найчастіший хештег", - "annual_report.summary.most_used_hashtag.none": "Немає", "annual_report.summary.new_posts.new_posts": "нові дописи", "annual_report.summary.percentile.text": "Це виводить вас у топ користувачів Mastodon.", "annual_report.summary.percentile.we_wont_tell_bernie": "Ми не скажемо Bernie.", - "annual_report.summary.thanks": "Дякуємо, що ви є частиною Mastodon!", "attachments_list.unprocessed": "(не оброблено)", "audio.hide": "Сховати аудіо", "block_modal.remote_users_caveat": "Ми попросимо сервер {domain} поважати ваше рішення. Однак дотримання вимог не гарантується, оскільки деякі сервери можуть обробляти блоки по-різному. Загальнодоступні дописи все ще можуть бути видимими для користувачів, які не увійшли в систему.", diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json index 79528fb9f61728..a5feaebc447724 100644 --- a/app/javascript/mastodon/locales/vi.json +++ b/app/javascript/mastodon/locales/vi.json @@ -114,29 +114,51 @@ "alt_text_modal.done": "Xong", "announcement.announcement": "Có gì mới?", "annual_report.announcement.action_build": "Tạo Wrapstodon của tôi", + "annual_report.announcement.action_dismiss": "Không, cảm ơn", "annual_report.announcement.action_view": "Xem Wrapstodon của tôi", "annual_report.announcement.description": "Tìm hiểu thêm về mức độ tương tác của bạn trên Mastodon trong năm qua.", "annual_report.announcement.title": "Wrapstodon {year} đã đến", - "annual_report.summary.archetype.booster": "Hiệp sĩ ngầu", - "annual_report.summary.archetype.lurker": "Kẻ rình mò", - "annual_report.summary.archetype.oracle": "Nhà tiên tri", - "annual_report.summary.archetype.pollster": "Chuyên gia khảo sát", - "annual_report.summary.archetype.replier": "Bướm xã hội", - "annual_report.summary.followers.followers": "người theo dõi", - "annual_report.summary.followers.total": "tổng {count}", - "annual_report.summary.here_it_is": "Nhìn lại năm {year} của bạn:", - "annual_report.summary.highlighted_post.by_favourites": "tút được thích nhiều nhất", - "annual_report.summary.highlighted_post.by_reblogs": "tút được đăng lại nhiều nhất", - "annual_report.summary.highlighted_post.by_replies": "tút được trả lời nhiều nhất", - "annual_report.summary.highlighted_post.possessive": "{name}", + "annual_report.nav_item.badge": "Mới", + "annual_report.shared_page.donate": "Quyên góp", + "annual_report.shared_page.footer": "Tạo bằng {heart} bởi đội ngũ Mastodon", + "annual_report.shared_page.footer_server_info": "{username} ở {domain}, một trong nhiều cộng đồng Mastodon.", + "annual_report.summary.archetype.booster.desc_public": "{name} thích tìm kiếm và đăng lại các tút, lan tỏa những người sáng tạo khác với mục tiêu hoàn hảo.", + "annual_report.summary.archetype.booster.desc_self": "Bạn thích tìm kiếm và đăng lại các tút, lan tỏa những người sáng tạo khác với mục tiêu hoàn hảo.", + "annual_report.summary.archetype.booster.name": "Cung Thủ", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.desc_public": "Chúng tôi biết {name} đang ở đâu đó ngoài kia, tận hưởng Mastodon theo cách riêng của họ.", + "annual_report.summary.archetype.lurker.desc_self": "Chúng tôi biết bạn đang ở đâu đó ngoài kia, tận hưởng Mastodon theo cách riêng.", + "annual_report.summary.archetype.lurker.name": "Lãng Nhân", + "annual_report.summary.archetype.oracle.desc_public": "{name} tạo ra nhiều tút mới hơn là trả lời, giúp Mastodon luôn mới mẻ và hướng tới tương lai.", + "annual_report.summary.archetype.oracle.desc_self": "Bạn tạo ra nhiều tút mới hơn là trả lời, giúp Mastodon luôn mới mẻ và hướng tới tương lai.", + "annual_report.summary.archetype.oracle.name": "Tiên Tri", + "annual_report.summary.archetype.pollster.desc_public": "{name} tạo ra nhiều cuộc vốt hơn các loại tút khác, khơi dậy sự tò mò trên Mastodon.", + "annual_report.summary.archetype.pollster.desc_self": "Bạn tạo ra nhiều cuộc vốt hơn các loại tút khác, khơi dậy sự tò mò trên Mastodon.", + "annual_report.summary.archetype.pollster.name": "Mọt Sách", + "annual_report.summary.archetype.replier.desc_public": "{name} thường xuyên trả lời tút của người khác, mang đến cho Mastodon những cuộc thảo luận mới.", + "annual_report.summary.archetype.replier.desc_self": "Bạn thường xuyên trả lời tút của người khác, mang đến cho Mastodon những cuộc thảo luận mới.", + "annual_report.summary.archetype.replier.name": "Bướm Xinh", + "annual_report.summary.archetype.reveal": "Bật mí nguyên mẫu của tôi", + "annual_report.summary.archetype.reveal_description": "Cảm ơn bạn đã là một phần của Mastodon! Đã đến lúc tìm hiểu xem bạn thuộc nguyên mẫu nào vào năm {year}.", + "annual_report.summary.archetype.title_public": "Nguyên mẫu của {name}", + "annual_report.summary.archetype.title_self": "Nguyên mẫu của bạn", + "annual_report.summary.close": "Đóng", + "annual_report.summary.copy_link": "Sao chép liên kết", + "annual_report.summary.followers.new_followers": "{count, plural, other {người theo dõi mới}}", + "annual_report.summary.highlighted_post.boost_count": "Tút này được đăng lại {count, plural, other {# lần}}.", + "annual_report.summary.highlighted_post.favourite_count": "Tút này được thích {count, plural, other {# lần}}.", + "annual_report.summary.highlighted_post.reply_count": "Tút này có {count, plural, other {# lượt trả lời}}.", + "annual_report.summary.highlighted_post.title": "Tút nổi tiếng nhất", "annual_report.summary.most_used_app.most_used_app": "app dùng nhiều nhất", "annual_report.summary.most_used_hashtag.most_used_hashtag": "hashtag dùng nhiều nhất", - "annual_report.summary.most_used_hashtag.none": "Không có", + "annual_report.summary.most_used_hashtag.used_count": "Bạn đã dùng hashtag này trong {count, plural, other {# tút}}.", + "annual_report.summary.most_used_hashtag.used_count_public": "{name} đã dùng hashtag này trong {count, plural, other {# tút}}.", "annual_report.summary.new_posts.new_posts": "tút mới", "annual_report.summary.percentile.text": "Bạn thuộc topthành viên của {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "Chúng tôi sẽ không kể cho Bernie.", + "annual_report.summary.share_elsewhere": "Chia sẻ nơi khác", "annual_report.summary.share_message": "Tôi là điển hình {archetype}!", - "annual_report.summary.thanks": "Cảm ơn đã trở thành một phần của Mastodon!", + "annual_report.summary.share_on_mastodon": "Chia sẻ lên Mastodon", "attachments_list.unprocessed": "(chưa xử lí)", "audio.hide": "Ẩn âm thanh", "block_modal.remote_users_caveat": "Chúng tôi sẽ yêu cầu {domain} tôn trọng quyết định của bạn. Tuy nhiên, việc tuân thủ không được đảm bảo vì một số máy chủ có thể xử lý việc chặn theo cách khác nhau. Các tút công khai vẫn có thể hiển thị đối với người dùng chưa đăng nhập.", @@ -419,6 +441,8 @@ "follow_suggestions.who_to_follow": "Gợi ý theo dõi", "followed_tags": "Hashtag theo dõi", "footer.about": "Giới thiệu", + "footer.about_mastodon": "Về Mastodon", + "footer.about_server": "Về {domain}", "footer.about_this_server": "Giới thiệu", "footer.directory": "Danh bạ", "footer.get_app": "Ứng dụng", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 274d3bc598e03a..232d8ec6774902 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -114,29 +114,51 @@ "alt_text_modal.done": "完成", "announcement.announcement": "公告", "annual_report.announcement.action_build": "构建我的 Wrapstodon 年度回顾", + "annual_report.announcement.action_dismiss": "不了,谢谢", "annual_report.announcement.action_view": "查看我的 Wrapstodon 年度回顾", "annual_report.announcement.description": "探索更多关于您过去一年在 Mastodon 上的互动情况。", "annual_report.announcement.title": "Wrapstodon {year} 年度回顾来啦", - "annual_report.summary.archetype.booster": "潮流捕手", - "annual_report.summary.archetype.lurker": "吃瓜群众", - "annual_report.summary.archetype.oracle": "未卜先知", - "annual_report.summary.archetype.pollster": "民调专家", - "annual_report.summary.archetype.replier": "社交蝴蝶", - "annual_report.summary.followers.followers": "关注者", - "annual_report.summary.followers.total": "共 {count} 人", - "annual_report.summary.here_it_is": "您的 {year} 年度回顾在此:", - "annual_report.summary.highlighted_post.by_favourites": "最受欢迎的嘟文", - "annual_report.summary.highlighted_post.by_reblogs": "传播最广的嘟文", - "annual_report.summary.highlighted_post.by_replies": "评论最多的嘟文", - "annual_report.summary.highlighted_post.possessive": "{name} 的", + "annual_report.nav_item.badge": "新", + "annual_report.shared_page.donate": "捐助", + "annual_report.shared_page.footer": "由 Mastodon 团队用 {heart} 生成", + "annual_report.shared_page.footer_server_info": "{username} 使用 {domain},运行 Mastodon 的众多社区之一。", + "annual_report.summary.archetype.booster.desc_public": "{name}持续寻找值得转嘟的嘟文,以精准眼光放大其他创作者的影响力。", + "annual_report.summary.archetype.booster.desc_self": "你持续寻找值得转嘟的嘟文,以精准眼光放大其他创作者的影响力。", + "annual_report.summary.archetype.booster.name": "转发游侠", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.desc_public": "我们知道{name}曾在联邦宇宙的某个角落驻足,以自己的方式静静享受着Mastodon。", + "annual_report.summary.archetype.lurker.desc_self": "我们知道你曾在联邦宇宙的某个角落驻足,以自己的方式静静享受着Mastodon。", + "annual_report.summary.archetype.lurker.name": "吃瓜群众", + "annual_report.summary.archetype.oracle.desc_public": "{name}发布的新嘟文远多于回复,为Mastodon注入无限活力与未来。", + "annual_report.summary.archetype.oracle.desc_self": "你发布的新嘟文远多于回复,为Mastodon注入无限活力与未来。", + "annual_report.summary.archetype.oracle.name": "创作先知", + "annual_report.summary.archetype.pollster.desc_public": "{name}发起投票的次数远多于其他嘟文类型,不断激发Mastodon上大家的好奇心。", + "annual_report.summary.archetype.pollster.desc_self": "你发起投票的次数远多于其他嘟文类型,不断激发Mastodon上大家的好奇心。", + "annual_report.summary.archetype.pollster.name": "探求先锋", + "annual_report.summary.archetype.replier.desc_public": "{name}经常回复其他人的嘟文,为Mastodon培育新讨论的萌芽。", + "annual_report.summary.archetype.replier.desc_self": "你经常回复其他人的嘟文,为Mastodon培育新讨论的萌芽。", + "annual_report.summary.archetype.replier.name": "社交蝴蝶", + "annual_report.summary.archetype.reveal": "揭示我的画像", + "annual_report.summary.archetype.reveal_description": "感谢你成为Mastodon的一份子!是时候揭晓你{year}年的用户画像啦。", + "annual_report.summary.archetype.title_public": "{name}的画像", + "annual_report.summary.archetype.title_self": "你的画像", + "annual_report.summary.close": "关闭", + "annual_report.summary.copy_link": "复制链接", + "annual_report.summary.followers.new_followers": "{count, plural, other {位新关注者}}", + "annual_report.summary.highlighted_post.boost_count": "此嘟文被转嘟了 {count, plural, other {# 次}}。", + "annual_report.summary.highlighted_post.favourite_count": "此嘟文收到了 {count, plural, other {# 次}}喜欢。", + "annual_report.summary.highlighted_post.reply_count": "此嘟文收到了 {count, plural, other {# 条回复}}。", + "annual_report.summary.highlighted_post.title": "最热门的嘟文", "annual_report.summary.most_used_app.most_used_app": "最常用的应用", "annual_report.summary.most_used_hashtag.most_used_hashtag": "最常用的话题", - "annual_report.summary.most_used_hashtag.none": "无", + "annual_report.summary.most_used_hashtag.used_count": "你在 {count, plural, other {# 条嘟文}}中使用了此话题标签。", + "annual_report.summary.most_used_hashtag.used_count_public": "{name} 在 {count, plural, other {# 条嘟文}}中使用了此话题标签。", "annual_report.summary.new_posts.new_posts": "新嘟文", "annual_report.summary.percentile.text": "这使你跻身 {domain} 用户的前", "annual_report.summary.percentile.we_wont_tell_bernie": " ", + "annual_report.summary.share_elsewhere": "分享到其他地方", "annual_report.summary.share_message": "我今年的画像是{archetype}!", - "annual_report.summary.thanks": "感谢您成为 Mastodon 的一员!", + "annual_report.summary.share_on_mastodon": "在Mastodon上分享", "attachments_list.unprocessed": "(未处理)", "audio.hide": "隐藏音频", "block_modal.remote_users_caveat": "我们将要求站点 {domain} 尊重你的决定。然而,我们无法保证对方一定遵从,因为某些站点可能会以不同的方案处理屏蔽操作。公开嘟文仍然可能对未登录用户可见。", @@ -419,6 +441,8 @@ "follow_suggestions.who_to_follow": "推荐关注", "followed_tags": "已关注话题", "footer.about": "关于", + "footer.about_mastodon": "关于 Mastodon", + "footer.about_server": "关于 {domain}", "footer.about_this_server": "关于本站", "footer.directory": "用户列表", "footer.get_app": "获取应用", diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json index dbb7752e97b662..8d86bd981d2afc 100644 --- a/app/javascript/mastodon/locales/zh-HK.json +++ b/app/javascript/mastodon/locales/zh-HK.json @@ -105,7 +105,6 @@ "alt_text_modal.cancel": "取消", "alt_text_modal.done": "完成", "announcement.announcement": "公告", - "annual_report.summary.thanks": "感謝您成為 Mastodon 的一份子!", "attachments_list.unprocessed": "(未處理)", "audio.hide": "隱藏音訊", "block_modal.remote_users_caveat": "我們會要求 {domain} 伺服器尊重你的決定。然而,由於部份伺服器可能以不同方式處理封鎖,因此無法保證一定會成功。公開帖文仍然有機會被未登入的使用者看見。", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 21e2bde3f9b9bd..78d191efa38123 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -114,29 +114,51 @@ "alt_text_modal.done": "完成", "announcement.announcement": "公告", "annual_report.announcement.action_build": "建立我的 Mastodon 年度回顧 (Wrapstodon)", + "annual_report.announcement.action_dismiss": "不需要,謝謝", "annual_report.announcement.action_view": "檢視我的 Mastodon 年度回顧 (Wrapstodon)", "annual_report.announcement.description": "探索更多關於您過去一年於 Mastodon 上的互動情況。", "annual_report.announcement.title": "您的 Mastodon 年度回顧 {year} 已抵達", - "annual_report.summary.archetype.booster": "酷炫獵人", - "annual_report.summary.archetype.lurker": "潛水高手", - "annual_report.summary.archetype.oracle": "先知", - "annual_report.summary.archetype.pollster": "民調專家", - "annual_report.summary.archetype.replier": "社交菁英", - "annual_report.summary.followers.followers": "跟隨者", - "annual_report.summary.followers.total": "總共 {count} 人", - "annual_report.summary.here_it_is": "以下是您的 {year} 年度回顧:", - "annual_report.summary.highlighted_post.by_favourites": "最多被加至最愛的嘟文", - "annual_report.summary.highlighted_post.by_reblogs": "最多轉嘟的嘟文", - "annual_report.summary.highlighted_post.by_replies": "最多回覆的嘟文", - "annual_report.summary.highlighted_post.possessive": "{name} 的", + "annual_report.nav_item.badge": "新報告", + "annual_report.shared_page.donate": "捐款", + "annual_report.shared_page.footer": "由 Mastodon 團隊 {heart} 產生", + "annual_report.shared_page.footer_server_info": "{username} 使用 {domain},運行 Mastodon 的眾多社群之一。", + "annual_report.summary.archetype.booster.desc_public": "{name} 持續追尋值得轉嘟的嘟文,以精準眼光放大其他創作者之影響力。", + "annual_report.summary.archetype.booster.desc_self": "您持續追尋值得轉嘟的嘟文,以精準眼光放大其他創作者之影響力。", + "annual_report.summary.archetype.booster.name": "弓箭手", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.desc_public": "我們知道 {name} 曾於聯邦宇宙的某個角落駐足,以靜謐的方式獨享 Mastodon。", + "annual_report.summary.archetype.lurker.desc_self": "我們知道您曾於聯邦宇宙的某個角落駐足,以靜謐的方式獨享 Mastodon。", + "annual_report.summary.archetype.lurker.name": "斯多葛主義信徒", + "annual_report.summary.archetype.oracle.desc_public": "{name} 發表的新嘟文多於回嘟,使 Mastodon 注入活力並展望未來。", + "annual_report.summary.archetype.oracle.desc_self": "您發表的新嘟文多於回嘟,使 Mastodon 注入活力並展望未來。", + "annual_report.summary.archetype.oracle.name": "先知", + "annual_report.summary.archetype.pollster.desc_public": "{name} 發起投票多於其他嘟文類型,使 Mastodon 充滿探索與好奇。", + "annual_report.summary.archetype.pollster.desc_self": "您發起投票多於其他嘟文類型,使 Mastodon 充滿探索與好奇。", + "annual_report.summary.archetype.pollster.name": "探究者", + "annual_report.summary.archetype.replier.desc_public": "{name} 經常回覆他人嘟文,替 Mastodon 帶來源源不絕的新討論。", + "annual_report.summary.archetype.replier.desc_self": "您經常回覆他人嘟文,替 Mastodon 帶來源源不絕的新討論。", + "annual_report.summary.archetype.replier.name": "花蝴蝶", + "annual_report.summary.archetype.reveal": "揭露我的類型稱號", + "annual_report.summary.archetype.reveal_description": "感謝您成為 Mastodon 的一分子!是時候揭曉您於 {year} 年份的代表類型。", + "annual_report.summary.archetype.title_public": "{name} 的類型稱號", + "annual_report.summary.archetype.title_self": "您的類型稱號", + "annual_report.summary.close": "關閉", + "annual_report.summary.copy_link": "複製連結", + "annual_report.summary.followers.new_followers": "{count, plural, other {位新跟隨者}}", + "annual_report.summary.highlighted_post.boost_count": "此嘟文被轉嘟 {count, plural, other {# 次}}。", + "annual_report.summary.highlighted_post.favourite_count": "此嘟文被加入最愛 {count, plural, other {# 次}}。", + "annual_report.summary.highlighted_post.reply_count": "此嘟文獲得 {count, plural, other {# 則回覆}}。", + "annual_report.summary.highlighted_post.title": "最受歡迎的嘟文", "annual_report.summary.most_used_app.most_used_app": "最常使用的應用程式", "annual_report.summary.most_used_hashtag.most_used_hashtag": "最常使用的主題標籤", - "annual_report.summary.most_used_hashtag.none": "無最常用之主題標籤", + "annual_report.summary.most_used_hashtag.used_count": "您於 {count, plural, other {# 則嘟文}}中使用此主題標籤。", + "annual_report.summary.most_used_hashtag.used_count_public": "{name} 於 {count, plural, other {# 則嘟文}}中使用此主題標籤。", "annual_report.summary.new_posts.new_posts": "新嘟文", "annual_report.summary.percentile.text": "這讓您成為前{domain} 的使用者。", "annual_report.summary.percentile.we_wont_tell_bernie": "我們不會告訴 Bernie。", + "annual_report.summary.share_elsewhere": "分享至其他地方", "annual_report.summary.share_message": "我獲得 {archetype} 稱號!", - "annual_report.summary.thanks": "感謝您成為 Mastodon 的一份子!", + "annual_report.summary.share_on_mastodon": "於 Mastodon 上分享", "attachments_list.unprocessed": "(未處理)", "audio.hide": "隱藏音訊", "block_modal.remote_users_caveat": "我們會要求 {domain} 伺服器尊重您的決定。然而,我們無法保證所有伺服器皆會遵守,某些伺服器可能以不同方式處理封鎖。未登入之使用者仍可能看見您的公開嘟文。", @@ -358,16 +380,16 @@ "empty_column.hashtag": "這個主題標籤下什麼也沒有。", "empty_column.home": "您的首頁時間軸是空的!跟隨更多人將它填滿吧!", "empty_column.list": "這份列表下什麼也沒有。當此列表的成員嘟出新的嘟文時,它們將顯示於此。", - "empty_column.mutes": "您尚未靜音任何使用者。", + "empty_column.mutes": "您還沒有靜音任何使用者。", "empty_column.notification_requests": "清空啦!已經沒有任何推播通知。當您收到新推播通知時,它們將依照您的設定於此顯示。", "empty_column.notifications": "您還沒有收到任何推播通知,當您與別人開始互動時,它將於此顯示。", "empty_column.public": "這裡什麼都沒有!嘗試寫些公開的嘟文,或著自己跟隨其他伺服器的使用者後就會有嘟文出現了", "error.no_hashtag_feed_access": "加入或登入 Mastodon 以檢視與跟隨此主題標籤。", "error.unexpected_crash.explanation": "由於發生系統故障或瀏覽器相容性問題,無法正常顯示此頁面。", "error.unexpected_crash.explanation_addons": "此頁面無法被正常顯示,這可能是由瀏覽器附加元件或網頁自動翻譯工具造成的。", - "error.unexpected_crash.next_steps": "請嘗試重新整理頁面。如果狀況沒有改善,您可以使用不同的瀏覽器或應用程式以檢視來使用 Mastodon。", - "error.unexpected_crash.next_steps_addons": "請嘗試關閉它們然後重新整理頁面。如果狀況沒有改善,您可以使用不同的瀏覽器或應用程式來檢視來使用 Mastodon。", - "errors.unexpected_crash.copy_stacktrace": "複製 stacktrace 到剪貼簿", + "error.unexpected_crash.next_steps": "請嘗試重新整理頁面。如果狀況沒有改善,您可以透過不同的瀏覽器或應用程式以使用 Mastodon。", + "error.unexpected_crash.next_steps_addons": "請嘗試關閉它們然後重新整理頁面。如果狀況沒有改善,您可以透過不同的瀏覽器或應用程式以使用 Mastodon。", + "errors.unexpected_crash.copy_stacktrace": "複製 stacktrace 至剪貼簿", "errors.unexpected_crash.report_issue": "回報問題", "explore.suggested_follows": "使用者", "explore.title": "熱門", @@ -381,12 +403,12 @@ "filter_modal.added.context_mismatch_title": "不符合情境!", "filter_modal.added.expired_explanation": "此過濾器類別已失效,您需要更新過期日期以套用。", "filter_modal.added.expired_title": "過期的過濾器!", - "filter_modal.added.review_and_configure": "要檢視與進一步設定此過濾器類別,請至 {settings_link}。", + "filter_modal.added.review_and_configure": "如欲檢視與進一步設定此過濾器類別,請至 {settings_link}。", "filter_modal.added.review_and_configure_title": "過濾器設定", "filter_modal.added.settings_link": "設定頁面", "filter_modal.added.short_explanation": "此嘟文已被新增至以下過濾器類別:{title}。", "filter_modal.added.title": "已新增過濾器!", - "filter_modal.select_filter.context_mismatch": "不是用目前情境", + "filter_modal.select_filter.context_mismatch": "不適用目前情境", "filter_modal.select_filter.expired": "已過期", "filter_modal.select_filter.prompt_new": "新類別:{name}", "filter_modal.select_filter.search": "搜尋或新增", @@ -404,13 +426,13 @@ "follow_requests.unlocked_explanation": "即便您的帳號未被鎖定,{domain} 的管理員認為您可能想要自己審核這些帳號的跟隨請求。", "follow_suggestions.curated_suggestion": "精選內容", "follow_suggestions.dismiss": "不再顯示", - "follow_suggestions.featured_longer": "{domain} 團隊精選", + "follow_suggestions.featured_longer": "{domain} 團隊精心挑選", "follow_suggestions.friends_of_friends_longer": "受您跟隨之使用者愛戴的風雲人物", - "follow_suggestions.hints.featured": "這個個人檔案是 {domain} 管理團隊精心挑選的。", - "follow_suggestions.hints.friends_of_friends": "這個個人檔案於您跟隨的帳號中很受歡迎。", - "follow_suggestions.hints.most_followed": "這個個人檔案是 {domain} 中最受歡迎的帳號之一。", - "follow_suggestions.hints.most_interactions": "這個個人檔案最近於 {domain} 受到非常多關注。", - "follow_suggestions.hints.similar_to_recently_followed": "這個個人檔案與您最近跟隨之帳號類似。", + "follow_suggestions.hints.featured": "此個人檔案是 {domain} 管理團隊精心挑選。", + "follow_suggestions.hints.friends_of_friends": "此個人檔案於您跟隨的帳號中很受歡迎。", + "follow_suggestions.hints.most_followed": "此個人檔案是 {domain} 中最受歡迎的帳號之一。", + "follow_suggestions.hints.most_interactions": "此個人檔案最近於 {domain} 受到非常多關注。", + "follow_suggestions.hints.similar_to_recently_followed": "此個人檔案與您最近跟隨之帳號類似。", "follow_suggestions.personalized_suggestion": "個人化推薦", "follow_suggestions.popular_suggestion": "熱門推薦", "follow_suggestions.popular_suggestion_longer": "{domain} 上的人氣王", @@ -419,6 +441,8 @@ "follow_suggestions.who_to_follow": "推薦跟隨帳號", "followed_tags": "已跟隨主題標籤", "footer.about": "關於", + "footer.about_mastodon": "關於 Mastodon", + "footer.about_server": "關於 {domain}", "footer.about_this_server": "關於", "footer.directory": "個人檔案目錄", "footer.get_app": "取得應用程式", @@ -615,7 +639,7 @@ "notification.admin.sign_up": "{name} 已經註冊", "notification.admin.sign_up.name_and_others": "{name} 與{count, plural, other {其他 # 個人}}已註冊", "notification.annual_report.message": "您的 {year} #Wrapstodon 正等著您!揭開您 Mastodon 上的年度精彩時刻與值得回憶的難忘時光!", - "notification.annual_report.view": "檢視 #Wrapstodon", + "notification.annual_report.view": "檢視 #Wrapstodon 年度回顧", "notification.favourite": "{name} 已將您的嘟文加入最愛", "notification.favourite.name_and_others_with_link": "{name} 與{count, plural, other {其他 # 個人}}已將您的嘟文加入最愛", "notification.favourite_pm": "{name} 將您的私訊加入最愛", diff --git a/app/javascript/mastodon/main.tsx b/app/javascript/mastodon/main.tsx index 249baf65fc0604..b4c45496745cee 100644 --- a/app/javascript/mastodon/main.tsx +++ b/app/javascript/mastodon/main.tsx @@ -30,7 +30,7 @@ function main() { } const { initializeEmoji } = await import('./features/emoji/index'); - initializeEmoji(); + await initializeEmoji(); const root = createRoot(mountNode); root.render(); diff --git a/app/javascript/mastodon/models/annual_report.ts b/app/javascript/mastodon/models/annual_report.ts index 819212efebcb72..863debfc1f358a 100644 --- a/app/javascript/mastodon/models/annual_report.ts +++ b/app/javascript/mastodon/models/annual_report.ts @@ -16,9 +16,9 @@ export interface TimeSeriesMonth { } export interface TopStatuses { - by_reblogs: number; - by_favourites: number; - by_replies: number; + by_reblogs: string; + by_favourites: string; + by_replies: string; } export type Archetype = @@ -55,5 +55,6 @@ export type AnnualReport = { schema_version: 2; data: AnnualReportV2; share_url: string | null; + account_id: string; } ); diff --git a/app/javascript/mastodon/models/notification_group.ts b/app/javascript/mastodon/models/notification_group.ts index edf576f5506b54..27a7388113b6d7 100644 --- a/app/javascript/mastodon/models/notification_group.ts +++ b/app/javascript/mastodon/models/notification_group.ts @@ -13,7 +13,7 @@ import type { import type { ApiReportJSON } from 'mastodon/api_types/reports'; // Maximum number of avatars displayed in a notification group -// This corresponds to the max lenght of `group.sampleAccountIds` +// This corresponds to the max length of `group.sampleAccountIds` export const NOTIFICATIONS_GROUP_MAX_AVATARS = 8; interface BaseNotificationGroup diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index 19f7286c05378c..8902cc3788357f 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -476,7 +476,9 @@ export const composeReducer = (state = initialState, action) => { switch(action.type) { case STORE_HYDRATE: - return hydrate(state, action.state.get('compose')); + if (action.state.get('compose')) + return hydrate(state, action.state.get('compose')); + return state; case COMPOSE_MOUNT: return state .set('mounted', state.get('mounted') + 1) diff --git a/app/javascript/mastodon/reducers/slices/annual_report.ts b/app/javascript/mastodon/reducers/slices/annual_report.ts index c01b8f79952f5a..798fe7cc9b0eb7 100644 --- a/app/javascript/mastodon/reducers/slices/annual_report.ts +++ b/app/javascript/mastodon/reducers/slices/annual_report.ts @@ -5,31 +5,31 @@ import { importFetchedAccounts, importFetchedStatuses, } from '@/mastodon/actions/importer'; -import { insertIntoTimeline } from '@/mastodon/actions/timelines'; import type { ApiAnnualReportState } from '@/mastodon/api/annual_report'; import { apiGetAnnualReport, apiGetAnnualReportState, apiRequestGenerateAnnualReport, } from '@/mastodon/api/annual_report'; +import { wrapstodon } from '@/mastodon/initial_state'; import type { AnnualReport } from '@/mastodon/models/annual_report'; - import { - createAppSelector, createAppThunk, createDataLoadingThunk, -} from '../../store/typed_functions'; - -export const TIMELINE_WRAPSTODON = 'inline-wrapstodon'; +} from '@/mastodon/store/typed_functions'; interface AnnualReportState { + year?: number; state?: ApiAnnualReportState; report?: AnnualReport; } const annualReportSlice = createSlice({ name: 'annualReport', - initialState: {} as AnnualReportState, + initialState: { + year: wrapstodon?.year, + state: wrapstodon?.state, + } as AnnualReportState, reducers: { setReport(state, action: PayloadAction) { state.report = action.payload; @@ -55,33 +55,27 @@ const annualReportSlice = createSlice({ export const annualReport = annualReportSlice.reducer; export const { setReport } = annualReportSlice.actions; -export const selectWrapstodonYear = createAppSelector( - [(state) => state.server.getIn(['server', 'wrapstodon'])], - (year: unknown) => (typeof year === 'number' && year > 2000 ? year : null), -); - -// This kicks everything off, and is called after fetching the server info. +// Called on initial load to check if we need to refresh the report state. export const checkAnnualReport = createAppThunk( `${annualReportSlice.name}/checkAnnualReport`, - async (_arg: unknown, { dispatch, getState }) => { - const year = selectWrapstodonYear(getState()); - if (!year) { + (_arg: unknown, { dispatch, getState }) => { + const { state, year } = getState().annualReport; + const me = getState().meta.get('me') as string; + + // If we have a state, we only need to fetch it again to poll for changes. + const needsStateRefresh = !state || state === 'generating'; + + if (!year || !me || !needsStateRefresh) { return; } - const state = await dispatch(fetchReportState()); - if ( - state.meta.requestStatus === 'fulfilled' && - state.payload !== 'ineligible' - ) { - dispatch(insertIntoTimeline('home', TIMELINE_WRAPSTODON, 1)); - } + void dispatch(fetchReportState()); }, ); const fetchReportState = createDataLoadingThunk( `${annualReportSlice.name}/fetchReportState`, async (_arg: unknown, { getState }) => { - const year = selectWrapstodonYear(getState()); + const { year } = getState().annualReport; if (!year) { throw new Error('Year is not set'); } @@ -92,8 +86,6 @@ const fetchReportState = createDataLoadingThunk( window.setTimeout(() => { void dispatch(fetchReportState()); }, 1_000 * refresh.retry); - } else if (state === 'available') { - void dispatch(getReport()); } return state; @@ -105,7 +97,7 @@ const fetchReportState = createDataLoadingThunk( export const generateReport = createDataLoadingThunk( `${annualReportSlice.name}/generateReport`, async (_arg: unknown, { getState }) => { - const year = selectWrapstodonYear(getState()); + const { year } = getState().annualReport; if (!year) { throw new Error('Year is not set'); } @@ -119,7 +111,7 @@ export const generateReport = createDataLoadingThunk( export const getReport = createDataLoadingThunk( `${annualReportSlice.name}/getReport`, async (_arg: unknown, { getState }) => { - const year = selectWrapstodonYear(getState()); + const { year } = getState().annualReport; if (!year) { throw new Error('Year is not set'); } diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js index b07281ab877827..e915fa7070a3cd 100644 --- a/app/javascript/mastodon/reducers/timelines.js +++ b/app/javascript/mastodon/reducers/timelines.js @@ -1,6 +1,6 @@ import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; -import { timelineDelete } from 'mastodon/actions/timelines_typed'; +import { timelineDelete, isNonStatusId } from 'mastodon/actions/timelines_typed'; import { blockAccountSuccess, @@ -19,7 +19,6 @@ import { TIMELINE_MARK_AS_PARTIAL, TIMELINE_INSERT, TIMELINE_GAP, - TIMELINE_SUGGESTIONS, disconnectTimeline, } from '../actions/timelines'; import { compareId } from '../compare_id'; @@ -36,7 +35,6 @@ const initialTimeline = ImmutableMap({ items: ImmutableList(), }); -const isPlaceholder = value => value === TIMELINE_GAP || value === TIMELINE_SUGGESTIONS; const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent, usePendingItems) => { // This method is pretty tricky because: @@ -69,20 +67,20 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is // First, find the furthest (if properly sorted, oldest) item in the timeline that is // newer than the oldest fetched one, as it's most likely that it delimits the gap. // Start the gap *after* that item. - const lastIndex = oldIds.findLastIndex(id => !isPlaceholder(id) && compareId(id, newIds.last()) >= 0) + 1; + const lastIndex = oldIds.findLastIndex(id => !isNonStatusId(id) && compareId(id, newIds.last()) >= 0) + 1; // Then, try to find the furthest (if properly sorted, oldest) item in the timeline that // is newer than the most recent fetched one, as it delimits a section comprised of only // items older or within `newIds` (or that were deleted from the server, so should be removed // anyway). // Stop the gap *after* that item. - const firstIndex = oldIds.take(lastIndex).findLastIndex(id => !isPlaceholder(id) && compareId(id, newIds.first()) > 0) + 1; + const firstIndex = oldIds.take(lastIndex).findLastIndex(id => !isNonStatusId(id) && compareId(id, newIds.first()) > 0) + 1; let insertedIds = ImmutableOrderedSet(newIds).withMutations(insertedIds => { // It is possible, though unlikely, that the slice we are replacing contains items older // than the elements we got from the API. Get them and add them back at the back of the // slice. - const olderIds = oldIds.slice(firstIndex, lastIndex).filter(id => !isPlaceholder(id) && compareId(id, newIds.last()) < 0); + const olderIds = oldIds.slice(firstIndex, lastIndex).filter(id => !isNonStatusId(id) && compareId(id, newIds.last()) < 0); insertedIds.union(olderIds); // Make sure we aren't inserting duplicates diff --git a/app/javascript/mastodon/settings.js b/app/javascript/mastodon/settings.js deleted file mode 100644 index f4883dc406cdb5..00000000000000 --- a/app/javascript/mastodon/settings.js +++ /dev/null @@ -1,51 +0,0 @@ -export default class Settings { - - constructor(keyBase = null) { - this.keyBase = keyBase; - } - - generateKey(id) { - return this.keyBase ? [this.keyBase, `id${id}`].join('.') : id; - } - - set(id, data) { - const key = this.generateKey(id); - try { - const encodedData = JSON.stringify(data); - localStorage.setItem(key, encodedData); - return data; - } catch { - return null; - } - } - - get(id) { - const key = this.generateKey(id); - try { - const rawData = localStorage.getItem(key); - return JSON.parse(rawData); - } catch { - return null; - } - } - - remove(id) { - const data = this.get(id); - if (data) { - const key = this.generateKey(id); - try { - localStorage.removeItem(key); - } catch { - // ignore if the key is not found - } - } - return data; - } - -} - -export const pushNotificationsSetting = new Settings('mastodon_push_notification_data'); -export const tagHistory = new Settings('mastodon_tag_history'); -export const bannerSettings = new Settings('mastodon_banner_settings'); -export const searchHistory = new Settings('mastodon_search_history'); -export const playerSettings = new Settings('mastodon_player'); diff --git a/app/javascript/mastodon/settings.ts b/app/javascript/mastodon/settings.ts new file mode 100644 index 00000000000000..87c67a6e04a466 --- /dev/null +++ b/app/javascript/mastodon/settings.ts @@ -0,0 +1,68 @@ +import type { RecentSearch } from './models/search'; + +export class Settings> { + keyBase: string | null; + + constructor(keyBase: string | null = null) { + this.keyBase = keyBase; + } + + private generateKey(id: string | number | symbol): string { + const idStr = typeof id === 'string' ? id : String(id); + return this.keyBase ? [this.keyBase, `id${idStr}`].join('.') : idStr; + } + + set(id: K, data: T[K]): T[K] | null { + const key = this.generateKey(id); + try { + const encodedData = JSON.stringify(data); + localStorage.setItem(key, encodedData); + return data; + } catch { + return null; + } + } + + get(id: K): T[K] | null { + const key = this.generateKey(id); + try { + const rawData = localStorage.getItem(key); + if (rawData === null) return null; + return JSON.parse(rawData) as T[K]; + } catch { + return null; + } + } + + remove(id: K): T[K] | null { + const data = this.get(id); + if (data !== null) { + const key = this.generateKey(id); + try { + localStorage.removeItem(key); + } catch { + // ignore if the key is not found + } + } + return data; + } +} + +export const pushNotificationsSetting = new Settings< + Record +>('mastodon_push_notification_data'); +export const tagHistory = new Settings>( + 'mastodon_tag_history', +); +export const bannerSettings = new Settings>( + 'mastodon_banner_settings', +); +export const searchHistory = new Settings>( + 'mastodon_search_history', +); +export const playerSettings = new Settings<{ volume: number; muted: boolean }>( + 'mastodon_player', +); +export const wrapstodonSettings = new Settings< + Record +>('wrapstodon'); diff --git a/app/javascript/mastodon/utils/types.ts b/app/javascript/mastodon/utils/types.ts index 24b9ee180f17fe..019b074813857b 100644 --- a/app/javascript/mastodon/utils/types.ts +++ b/app/javascript/mastodon/utils/types.ts @@ -14,3 +14,11 @@ export type SomeRequired = T & Required>; export type SomeOptional = Pick> & Partial>; + +export type RequiredExcept = SomeOptional, K>; + +export type OmitValueType = { + [K in keyof T as T[K] extends V ? never : K]: T[K]; +}; + +export type AnyFunction = (...args: never) => unknown; diff --git a/app/javascript/styles/common.scss b/app/javascript/styles/common.scss index 5f5077627ef2e6..aca615414f17aa 100644 --- a/app/javascript/styles/common.scss +++ b/app/javascript/styles/common.scss @@ -16,7 +16,6 @@ @use 'mastodon/polls'; @use 'mastodon/modal'; @use 'mastodon/emoji_picker'; -@use 'mastodon/annual_reports'; @use 'mastodon/about'; @use 'mastodon/tables'; @use 'mastodon/admin'; diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index ca9bf36e7b4c77..483d0fdfc29393 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -164,6 +164,7 @@ $content-width: 840px; width: 100%; max-width: $content-width; flex: 1 1 auto; + isolation: isolate; } @media screen and (max-width: ($content-width + $sidebar-width)) { diff --git a/app/javascript/styles/mastodon/annual_reports.scss b/app/javascript/styles/mastodon/annual_reports.scss deleted file mode 100644 index a9b7e0ddee0120..00000000000000 --- a/app/javascript/styles/mastodon/annual_reports.scss +++ /dev/null @@ -1,342 +0,0 @@ -@use 'variables' as *; - -:root { - --indigo-1: #17063b; - --indigo-2: #2f0c7a; - --indigo-3: #562cfc; - --indigo-5: #858afa; - --indigo-6: #cccfff; - --lime: #baff3b; - --goldenrod-2: #ffc954; -} - -.annual-report { - flex: 0 0 auto; - background: var(--indigo-1); - padding: 24px; - - &__header { - margin-bottom: 16px; - - h1 { - font-size: 25px; - font-weight: 600; - line-height: 30px; - color: var(--lime); - margin-bottom: 8px; - } - - p { - font-size: 16px; - font-weight: 600; - line-height: 20px; - color: var(--indigo-6); - } - } - - &__bento { - display: grid; - gap: 8px; - grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr); - grid-template-rows: minmax(0, auto) minmax(0, 1fr) minmax(0, auto) minmax( - 0, - auto - ); - - &__box { - padding: 16px; - border-radius: 8px; - background: var(--indigo-2); - color: var(--indigo-5); - } - } - - &__summary { - &__most-boosted-post { - grid-column: span 2; - grid-row: span 2; - padding: 0; - - .status__content, - .content-warning { - color: var(--indigo-6); - } - - .detailed-status { - border: 0; - } - - .content-warning { - border: 0; - background: var(--indigo-1); - - .link-button { - color: var(--indigo-5); - } - } - - .detailed-status__meta__line { - border-bottom-color: var(--indigo-3); - } - - .detailed-status__meta { - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - } - - .detailed-status__meta, - .poll__footer, - .poll__link, - .detailed-status .logo, - .detailed-status__display-name { - color: var(--indigo-5); - } - - .detailed-status__meta .animated-number, - .detailed-status__display-name strong { - color: var(--indigo-6); - } - - .poll__chart { - background-color: var(--indigo-3); - - &.leading { - background-color: var(--goldenrod-2); - } - } - - .status-card, - .hashtag-bar { - display: none; - } - } - - &__followers { - grid-column: span 1; - text-align: center; - position: relative; - overflow: hidden; - padding-block-start: 24px; - padding-block-end: 24px; - - --sparkline-gradient-top: rgba(86, 44, 252, 50%); - --sparkline-gradient-bottom: rgba(86, 44, 252, 0%); - - &__foreground { - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 8px; - position: relative; - z-index: 1; - } - - &__number { - font-size: 31px; - font-weight: 600; - line-height: 37px; - color: var(--lime); - } - - &__label { - font-size: 14px; - font-weight: 600; - line-height: 17px; - color: var(--indigo-6); - } - - &__footnote { - display: block; - font-weight: 400; - opacity: 0.5; - } - - svg { - position: absolute; - bottom: 0; - inset-inline-end: 0; - pointer-events: none; - z-index: 0; - height: 70%; - width: auto; - - path:first-child { - fill: url('#gradient') !important; - fill-opacity: 1 !important; - } - - path:last-child { - stroke: var(--color-graph-primary-stroke) !important; - fill: none !important; - } - } - } - - &__archetype { - grid-column: span 1; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - text-align: center; - gap: 8px; - padding: 0; - - img { - display: block; - width: 100%; - height: auto; - border-radius: 8px; - } - - &__label { - padding: 16px; - padding-bottom: 8px; - font-size: 14px; - line-height: 17px; - font-weight: 600; - color: var(--lime); - } - } - - &__most-used-app { - grid-column: span 1; - text-align: center; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 8px; - box-sizing: border-box; - - &__label { - font-size: 14px; - line-height: 17px; - font-weight: 600; - color: var(--indigo-6); - } - - &__icon { - font-size: 14px; - line-height: 17px; - font-weight: 600; - color: var(--goldenrod-2); - } - } - - &__percentile { - grid-row: span 2; - display: flex; - flex-direction: column; - align-items: center; - justify-content: space-between; - text-align: center; - text-wrap: balance; - padding: 16px 8px; - - &__label { - font-size: 14px; - line-height: 17px; - } - - &__number { - font-size: 54px; - font-weight: 600; - line-height: 73px; - color: var(--goldenrod-2); - } - - &__footnote { - font-size: 11px; - line-height: 14px; - opacity: 0.5; - } - } - - &__new-posts { - grid-column: span 2; - text-align: center; - position: relative; - overflow: hidden; - - &__label { - font-size: 20px; - font-weight: 600; - line-height: 24px; - color: var(--indigo-6); - z-index: 1; - position: relative; - } - - &__number { - font-size: 76px; - font-weight: 600; - line-height: 91px; - color: var(--goldenrod-2); - z-index: 1; - position: relative; - } - - svg { - position: absolute; - inset-inline-start: -7px; - top: -4px; - z-index: 0; - } - } - - &__most-used-hashtag { - grid-column: span 2; - text-align: center; - overflow: hidden; - - &__hashtag { - font-size: 42px; - font-weight: 600; - line-height: 58px; - color: var(--indigo-6); - margin-inline-start: -100%; - margin-inline-end: -100%; - } - - &__label { - font-size: 14px; - font-weight: 600; - line-height: 17px; - } - } - } -} - -.annual-report-modal { - max-width: 600px; - background: var(--indigo-1); - border-radius: 16px; - display: flex; - flex-direction: column; - overflow-y: auto; - - .loading-indicator .circular-progress { - color: var(--lime); - } - - @media screen and (max-width: $no-columns-breakpoint) { - border-bottom: 0; - border-radius: 16px 16px 0 0; - } -} - -.notification-group--annual-report { - .notification-group__icon { - color: var(--lime); - } - - .notification-group__main .link-button { - font-weight: 500; - color: var(--lime); - } -} diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 1d3c30b6295eef..ab92226efb6695 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -784,7 +784,6 @@ body > [data-popper-placement] { align-items: center; flex: 1 1 auto; max-width: 100%; - overflow: hidden; } &__buttons { @@ -948,6 +947,11 @@ body > [data-popper-placement] { text-overflow: ellipsis; white-space: nowrap; + &:focus-visible { + outline: var(--outline-focus-default); + outline-offset: 2px; + } + &[disabled] { cursor: default; color: var(--color-text-disabled); @@ -2938,7 +2942,7 @@ a.account__display-name { cursor: default; &:focus { - color: rgb(from var(--color-text-disabled) r g b / 70%); + color: var(--color-text-on-disabled); background: var(--color-bg-disabled); outline: 0; } @@ -4103,8 +4107,8 @@ a.account__display-name { box-sizing: border-box; &:hover, - &:focus, - &:active { + &:active, + &:focus-visible { color: var(--color-text-primary); } @@ -4122,14 +4126,7 @@ a.account__display-name { } &--logo { - background: transparent; padding: 10px; - - &:hover, - &:focus, - &:active { - background: transparent; - } } } diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index 825d4784bca678..a78ea77b8d2c2b 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -728,6 +728,17 @@ code { } } + .horizontal-options .label_input__wrapper { + display: flex; + flex-wrap: wrap; + gap: 20px; + + .radio, + .radio > label { + margin-bottom: 0; + } + } + .status-card { contain: unset; } diff --git a/app/javascript/styles/mastodon/theme/index.scss b/app/javascript/styles/mastodon/theme/index.scss index 8e275e514a063c..a907299887d1be 100644 --- a/app/javascript/styles/mastodon/theme/index.scss +++ b/app/javascript/styles/mastodon/theme/index.scss @@ -6,7 +6,7 @@ html { @include base.palette; - &[data-user-theme='system'] { + &:where([data-user-theme='system']) { color-scheme: dark light; @media (prefers-color-scheme: dark) { diff --git a/app/javascript/testing/factories.ts b/app/javascript/testing/factories.ts index ec0436dc4dbff9..baf0ca01df02dc 100644 --- a/app/javascript/testing/factories.ts +++ b/app/javascript/testing/factories.ts @@ -1,4 +1,4 @@ -import { Map as ImmutableMap } from 'immutable'; +import { Map as ImmutableMap, List } from 'immutable'; import type { ApiRelationshipJSON } from '@/mastodon/api_types/relationships'; import type { ApiStatusJSON } from '@/mastodon/api_types/statuses'; @@ -7,6 +7,7 @@ import type { UnicodeEmojiData, } from '@/mastodon/features/emoji/types'; import { createAccountFromServerJSON } from '@/mastodon/models/account'; +import type { AnnualReport } from '@/mastodon/models/annual_report'; import type { Status } from '@/mastodon/models/status'; import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; @@ -94,16 +95,18 @@ export const statusFactory: FactoryFunction = ({ mentions: [], tags: [], emojis: [], - content: '

This is a test status.

', + contentHtml: data.text ?? '

This is a test status.

', ...data, }); export const statusFactoryState = ( options: FactoryOptions = {}, ) => - ImmutableMap( - statusFactory(options) as unknown as Record, - ) as unknown as Status; + ImmutableMap({ + ...(statusFactory(options) as unknown as Record), + account: options.account?.id ?? '1', + tags: List(options.tags), + }) as unknown as Status; export const relationshipsFactory: FactoryFunction = ({ id, @@ -134,6 +137,7 @@ export function unicodeEmojiFactory( hexcode: 'test', label: 'Test', unicode: '🧪', + shortcodes: ['test_emoji'], ...data, }; } @@ -143,9 +147,125 @@ export function customEmojiFactory( ): CustomEmojiData { return { shortcode: 'custom', - static_url: 'emoji/custom/static', - url: 'emoji/custom', + static_url: '/custom-emoji/logo.svg', + url: '/custom-emoji/logo.svg', visible_in_picker: true, ...data, }; } + +interface AnnualReportState { + state: 'available'; + report: AnnualReport; +} + +interface AnnualReportFactoryOptions { + account_id?: string; + status_id?: string; + archetype?: AnnualReport['data']['archetype']; + year?: number; + top_hashtag?: AnnualReport['data']['top_hashtags'][0]; + without_posts?: boolean; +} + +export function annualReportFactory({ + account_id = '1', + status_id = '1', + archetype = 'lurker', + year, + top_hashtag, + without_posts = false, +}: AnnualReportFactoryOptions = {}): AnnualReportState { + return { + state: 'available', + report: { + schema_version: 2, + share_url: '#', + account_id, + year: year ?? 2025, + data: { + archetype, + time_series: [ + { + month: 1, + statuses: 0, + followers: 0, + following: 0, + }, + { + month: 2, + statuses: 0, + followers: 0, + following: 0, + }, + { + month: 3, + statuses: 0, + followers: 0, + following: 0, + }, + { + month: 4, + statuses: 0, + followers: 0, + following: 0, + }, + { + month: 5, + statuses: without_posts ? 0 : 1, + followers: 1, + following: 3, + }, + { + month: 6, + statuses: without_posts ? 0 : 7, + followers: 1, + following: 0, + }, + { + month: 7, + statuses: without_posts ? 0 : 2, + followers: 0, + following: 0, + }, + { + month: 8, + statuses: without_posts ? 0 : 2, + followers: 0, + following: 0, + }, + { + month: 9, + statuses: without_posts ? 0 : 11, + followers: 0, + following: 1, + }, + { + month: 10, + statuses: without_posts ? 0 : 12, + followers: 0, + following: 1, + }, + { + month: 11, + statuses: without_posts ? 0 : 6, + followers: 0, + following: 1, + }, + { + month: 12, + statuses: without_posts ? 0 : 4, + followers: 0, + following: 0, + }, + ], + top_hashtags: top_hashtag ? [top_hashtag] : [], + top_statuses: { + by_reblogs: status_id, + by_replies: status_id, + by_favourites: status_id, + }, + }, + }, + }; +} diff --git a/app/lib/activitypub/parser/interaction_policy_parser.rb b/app/lib/activitypub/parser/interaction_policy_parser.rb new file mode 100644 index 00000000000000..6587b245eeac85 --- /dev/null +++ b/app/lib/activitypub/parser/interaction_policy_parser.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +class ActivityPub::Parser::InteractionPolicyParser + def initialize(json, account) + @json = json + @account = account + end + + def bitmap + flags = 0 + return flags if @json.blank? + + flags |= subpolicy(@json['automaticApproval']) + flags <<= 16 + flags |= subpolicy(@json['manualApproval']) + + flags + end + + private + + def subpolicy(partial_json) + flags = 0 + + allowed_actors = Array(partial_json).dup + allowed_actors.uniq! + + flags |= InteractionPolicy::POLICY_FLAGS[:public] if allowed_actors.delete('as:Public') || allowed_actors.delete('Public') || allowed_actors.delete('https://www.w3.org/ns/activitystreams#Public') + flags |= InteractionPolicy::POLICY_FLAGS[:followers] if allowed_actors.delete(@account.followers_url) + flags |= InteractionPolicy::POLICY_FLAGS[:following] if allowed_actors.delete(@account.following_url) + + includes_target_actor = allowed_actors.delete(ActivityPub::TagManager.instance.uri_for(@account)).present? + + # Any unrecognized actor is marked as unsupported + flags |= InteractionPolicy::POLICY_FLAGS[:unsupported_policy] unless allowed_actors.empty? + + flags |= InteractionPolicy::POLICY_FLAGS[:disabled] if flags.zero? && includes_target_actor + + flags + end +end diff --git a/app/lib/activitypub/parser/status_parser.rb b/app/lib/activitypub/parser/status_parser.rb index 020bed69e0cb0c..f9daef267a6b5a 100644 --- a/app/lib/activitypub/parser/status_parser.rb +++ b/app/lib/activitypub/parser/status_parser.rb @@ -208,15 +208,15 @@ def quote_subpolicy(subpolicy) allowed_actors = as_array(subpolicy).dup allowed_actors.uniq! - flags |= Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] if allowed_actors.delete('as:Public') || allowed_actors.delete('Public') || allowed_actors.delete('https://www.w3.org/ns/activitystreams#Public') - flags |= Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] if allowed_actors.delete(@options[:followers_collection]) - flags |= Status::QUOTE_APPROVAL_POLICY_FLAGS[:following] if allowed_actors.delete(@options[:following_collection]) + flags |= InteractionPolicy::POLICY_FLAGS[:public] if allowed_actors.delete('as:Public') || allowed_actors.delete('Public') || allowed_actors.delete('https://www.w3.org/ns/activitystreams#Public') + flags |= InteractionPolicy::POLICY_FLAGS[:followers] if allowed_actors.delete(@options[:followers_collection]) + flags |= InteractionPolicy::POLICY_FLAGS[:following] if allowed_actors.delete(@options[:following_collection]) # Remove the special-meaning actor URI allowed_actors.delete(@options[:actor_uri]) # Any unrecognized actor is marked as unsupported - flags |= Status::QUOTE_APPROVAL_POLICY_FLAGS[:unsupported_policy] unless allowed_actors.empty? + flags |= InteractionPolicy::POLICY_FLAGS[:unsupported_policy] unless allowed_actors.empty? flags end diff --git a/app/lib/annual_report.rb b/app/lib/annual_report.rb index a9c9135ed46377..689fbca399f74e 100644 --- a/app/lib/annual_report.rb +++ b/app/lib/annual_report.rb @@ -17,7 +17,7 @@ def self.table_name_prefix end def self.current_campaign - return unless Mastodon::Feature.wrapstodon_enabled? + return unless Setting.wrapstodon datetime = Time.now.utc datetime.year if datetime.month == 12 && (10..31).cover?(datetime.day) @@ -34,6 +34,25 @@ def eligible? end end + def state + return 'available' if GeneratedAnnualReport.exists?(account_id: @account.id, year: @year) + + async_refresh = AsyncRefresh.new(refresh_key) + + if async_refresh.running? + yield async_refresh if block_given? + 'generating' + elsif AnnualReport.current_campaign == @year && eligible? + 'eligible' + else + 'ineligible' + end + end + + def refresh_key + "wrapstodon:#{@account.id}:#{@year}" + end + def generate return if GeneratedAnnualReport.exists?(account: @account, year: @year) diff --git a/app/lib/annual_report/time_series.rb b/app/lib/annual_report/time_series.rb index 3f9f0d52e88da7..fc2c5e2ce469ad 100644 --- a/app/lib/annual_report/time_series.rb +++ b/app/lib/annual_report/time_series.rb @@ -3,29 +3,24 @@ class AnnualReport::TimeSeries < AnnualReport::Source def generate { - time_series: (1..12).map do |month| - { - month: month, - statuses: statuses_per_month[month] || 0, - following: following_per_month[month] || 0, - followers: followers_per_month[month] || 0, - } - end, + time_series: [ + { + month: 12, + statuses: statuses_this_year, + followers: followers_this_year, + }, + ], } end private - def statuses_per_month - @statuses_per_month ||= report_statuses.group(:period).pluck(date_part_month.as('period'), Arel.star.count).to_h + def statuses_this_year + @statuses_this_year ||= report_statuses.count end - def following_per_month - @following_per_month ||= annual_relationships_by_month(@account.active_relationships) - end - - def followers_per_month - @followers_per_month ||= annual_relationships_by_month(@account.passive_relationships) + def followers_this_year + @followers_this_year ||= @account.passive_relationships.where(created_in_year, @year).count end def date_part_month @@ -34,14 +29,6 @@ def date_part_month SQL end - def annual_relationships_by_month(relationships) - relationships - .where(created_in_year, @year) - .group(:period) - .pluck(date_part_month.as('period'), Arel.star.count) - .to_h - end - def created_in_year Arel.sql(<<~SQL.squish) DATE_PART('year', created_at) = ? diff --git a/app/lib/annual_report/top_statuses.rb b/app/lib/annual_report/top_statuses.rb index 4dcc31892bbf23..06c53e2a487f32 100644 --- a/app/lib/annual_report/top_statuses.rb +++ b/app/lib/annual_report/top_statuses.rb @@ -5,14 +5,14 @@ def generate { top_statuses: { by_reblogs: status_identifier(most_reblogged_status), - by_favourites: status_identifier(most_favourited_status), - by_replies: status_identifier(most_replied_status), + by_favourites: nil, + by_replies: nil, }, } end def eligible? - report_statuses.public_visibility.exists? + report_statuses.distributable_visibility.exists? end private @@ -43,7 +43,7 @@ def most_replied_status def base_scope report_statuses - .public_visibility + .distributable_visibility .joins(:status_stat) end end diff --git a/app/lib/application_extension.rb b/app/lib/application_extension.rb index d8090d15bcba33..bc6c7561cca464 100644 --- a/app/lib/application_extension.rb +++ b/app/lib/application_extension.rb @@ -28,7 +28,7 @@ def confirmation_redirect_uri end def redirect_uris - # Doorkeeper stores the redirect_uri value as a newline delimeted list in + # Doorkeeper stores the redirect_uri value as a newline delimited list in # the database: redirect_uri.split end diff --git a/app/lib/interaction_policy.rb b/app/lib/interaction_policy.rb new file mode 100644 index 00000000000000..8d0b8dd060b47c --- /dev/null +++ b/app/lib/interaction_policy.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +class InteractionPolicy + POLICY_FLAGS = { + unsupported_policy: (1 << 0), # Not supported by Mastodon + public: (1 << 1), # Everyone is allowed to interact + followers: (1 << 2), # Only followers may interact + following: (1 << 3), # Only accounts followed by the target may interact + disabled: (1 << 4), # All interaction explicitly disabled + }.freeze + + class SubPolicy + def initialize(bitmap) + @bitmap = bitmap + end + + def as_keys + POLICY_FLAGS.keys.select { |key| @bitmap.anybits?(POLICY_FLAGS[key]) }.map(&:to_s) + end + + POLICY_FLAGS.each_key do |key| + define_method :"#{key}?" do + @bitmap.anybits?(POLICY_FLAGS[key]) + end + end + + def missing? + @bitmap.zero? + end + end + + attr_reader :automatic, :manual + + def initialize(bitmap) + @bitmap = bitmap + @automatic = SubPolicy.new(@bitmap >> 16) + @manual = SubPolicy.new(@bitmap & 0xFFFF) + end +end diff --git a/app/lib/request.rb b/app/lib/request.rb index dd65b481d88320..06c917c4266e02 100644 --- a/app/lib/request.rb +++ b/app/lib/request.rb @@ -275,7 +275,7 @@ def require_limit_not_exceeded!(limit) end if ::HTTP::Response.methods.include?(:body_with_limit) && !Rails.env.production? - abort 'HTTP::Response#body_with_limit is already defined, the monkey patch will not be applied' + raise 'HTTP::Response#body_with_limit is already defined, the monkey patch will not be applied' else class ::HTTP::Response include Request::ClientLimit diff --git a/app/lib/status_filter.rb b/app/lib/status_filter.rb index 7672c6402e1906..6987fa872e0097 100644 --- a/app/lib/status_filter.rb +++ b/app/lib/status_filter.rb @@ -3,11 +3,9 @@ class StatusFilter attr_reader :status, :account - def initialize(status, account, preloaded_relations = {}, preloaded_status_relations = {}) + def initialize(status, account) @status = status @account = account - @preloaded_relations = preloaded_relations - @preloaded_status_relations = preloaded_status_relations end def filtered? @@ -41,15 +39,15 @@ def filtered_status? end def blocking_account? - @preloaded_relations[:blocking] ? @preloaded_relations[:blocking][status.account_id] : account.blocking?(status.account_id) + account.blocking?(status.account_id) end def blocking_domain? - @preloaded_relations[:domain_blocking_by_domain] ? @preloaded_relations[:domain_blocking_by_domain][status.account_domain] : account.domain_blocking?(status.account_domain) + account.domain_blocking?(status.account_domain) end def muting_account? - @preloaded_relations[:muting] ? @preloaded_relations[:muting][status.account_id] : account.muting?(status.account_id) + account.muting?(status.account_id) end def silenced_account? @@ -61,7 +59,7 @@ def status_account_silenced? end def account_following_status_account? - @preloaded_relations[:following] ? @preloaded_relations[:following][status.account_id] : account&.following?(status.account_id) + account&.following?(status.account_id) end def blocked_by_policy? @@ -69,6 +67,6 @@ def blocked_by_policy? end def policy_allows_show? - StatusPolicy.new(account, status, @preloaded_relations, @preloaded_status_relations).show? + StatusPolicy.new(account, status).show? end end diff --git a/app/models/account.rb b/app/models/account.rb index 783a42bbdc76f4..ad91bbb6696f5d 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -5,58 +5,59 @@ # Table name: accounts # # id :bigint(8) not null, primary key -# username :string default(""), not null -# domain :string -# private_key :text -# public_key :text default(""), not null -# created_at :datetime not null -# updated_at :datetime not null -# note :text default(""), not null -# display_name :string default(""), not null -# uri :string default(""), not null -# url :string -# avatar_file_name :string +# actor_type :string +# also_known_as :string is an Array +# attribution_domains :string default([]), is an Array # avatar_content_type :string +# avatar_file_name :string +# avatar_file_size :integer +# avatar_remote_url :string +# avatar_storage_schema_version :integer # avatar_updated_at :datetime -# header_file_name :string +# discoverable :boolean +# display_name :string default(""), not null +# domain :string +# feature_approval_policy :integer default(0), not null +# featured_collection_url :string +# fields :jsonb +# followers_url :string default(""), not null +# following_url :string default(""), not null # header_content_type :string -# header_updated_at :datetime -# avatar_remote_url :string -# locked :boolean default(FALSE), not null +# header_file_name :string +# header_file_size :integer # header_remote_url :string default(""), not null -# last_webfingered_at :datetime +# header_storage_schema_version :integer +# header_updated_at :datetime +# hide_collections :boolean +# id_scheme :integer default("numeric_ap_id") # inbox_url :string default(""), not null +# indexable :boolean default(FALSE), not null +# last_webfingered_at :datetime +# locked :boolean default(FALSE), not null +# master_settings :jsonb +# memorial :boolean default(FALSE), not null +# note :text default(""), not null # outbox_url :string default(""), not null -# shared_inbox_url :string default(""), not null -# followers_url :string default(""), not null -# following_url :string default(""), not null +# private_key :text # protocol :integer default("ostatus"), not null -# memorial :boolean default(FALSE), not null -# moved_to_account_id :bigint(8) -# featured_collection_url :string -# fields :jsonb -# actor_type :string -# discoverable :boolean -# also_known_as :string is an Array +# public_key :text default(""), not null +# remote_pending :boolean default(FALSE), not null +# requested_review_at :datetime +# reviewed_at :datetime +# searchability :integer default("direct"), not null +# sensitized_at :datetime +# settings :jsonb +# shared_inbox_url :string default(""), not null # silenced_at :datetime # suspended_at :datetime -# hide_collections :boolean -# avatar_storage_schema_version :integer -# header_storage_schema_version :integer # suspension_origin :integer -# sensitized_at :datetime # trendable :boolean -# reviewed_at :datetime -# requested_review_at :datetime -# searchability :integer default("direct"), not null -# settings :jsonb -# indexable :boolean default(FALSE), not null -# master_settings :jsonb -# remote_pending :boolean default(FALSE), not null -# avatar_file_size :integer -# header_file_size :integer -# attribution_domains :string default([]), is an Array -# id_scheme :integer default("numeric_ap_id") +# uri :string default(""), not null +# url :string +# username :string default(""), not null +# created_at :datetime not null +# updated_at :datetime not null +# moved_to_account_id :bigint(8) # class Account < ApplicationRecord @@ -94,6 +95,7 @@ class Account < ApplicationRecord include Account::FaspConcern include Account::FinderConcern include Account::Header + include Account::InteractionPolicyConcern include Account::Interactions include Account::Mappings include Account::Merging @@ -498,6 +500,10 @@ def compute_searchability_activitypub local? ? 'public' : searchability end + def featureable? + local? && discoverable? + end + private def prepare_contents diff --git a/app/models/admin/ng_rule.rb b/app/models/admin/ng_rule.rb index 594403a3766053..32d94999e1e6cd 100644 --- a/app/models/admin/ng_rule.rb +++ b/app/models/admin/ng_rule.rb @@ -99,7 +99,7 @@ def check_reaction_or_record! text = @options[:target_status].present? ? [@options[:target_status].spoiler_text, @options[:target_status].text].compact_blank.join("\n\n") : nil data = { - url: @options[:target_status].present? ? @options[:target_status].url : nil, + url: @options[:target_status].presence&.url, } record!('reaction', @options[:uri], "reaction_#{@options[:reaction_type]}", text: text, data: data) if !@account.local? || @ng_rule.record_history_also_local diff --git a/app/models/bulk_import.rb b/app/models/bulk_import.rb index e3e46d7b1c6310..8435c245a23fcc 100644 --- a/app/models/bulk_import.rb +++ b/app/models/bulk_import.rb @@ -53,7 +53,7 @@ def self.progress!(bulk_import_id, imported: false) BulkImport.increment_counter(:processed_items, bulk_import_id) BulkImport.increment_counter(:imported_items, bulk_import_id) if imported - # Since the incrementation has been done atomically, concurrent access to `bulk_import` is now bening + # Since the incrementation has been done atomically, concurrent access to `bulk_import` is now benign bulk_import = BulkImport.find(bulk_import_id) bulk_import.update!(state: :finished, finished_at: Time.now.utc) if bulk_import.processed_items == bulk_import.total_items end diff --git a/app/models/collection.rb b/app/models/collection.rb index 231f12ef5219ab..2e352cbe8761cd 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -7,6 +7,7 @@ # id :bigint(8) not null, primary key # description :text not null # discoverable :boolean not null +# item_count :integer default(0), not null # local :boolean not null # name :string not null # original_number_of_items :integer @@ -39,11 +40,6 @@ class Collection < ApplicationRecord validate :items_do_not_exceed_limit scope :with_items, -> { includes(:collection_items).merge(CollectionItem.with_accounts) } - scope :with_item_count, lambda { - select('collections.*, COUNT(collection_items.id)') - .left_joins(:collection_items) - .group(collections: :id) - } scope :with_tag, -> { includes(:tag) } def remote? diff --git a/app/models/collection_item.rb b/app/models/collection_item.rb index 48a18592fdad1a..5c624165e62cba 100644 --- a/app/models/collection_item.rb +++ b/app/models/collection_item.rb @@ -17,7 +17,7 @@ # collection_id :bigint(8) not null # class CollectionItem < ApplicationRecord - belongs_to :collection + belongs_to :collection, counter_cache: :item_count belongs_to :account, optional: true enum :state, @@ -32,6 +32,8 @@ class CollectionItem < ApplicationRecord validates :account, presence: true, if: :accepted? validates :object_uri, presence: true, if: -> { account.nil? } + before_validation :set_position, on: :create + scope :ordered, -> { order(position: :asc) } scope :with_accounts, -> { includes(account: [:account_stat, :user]) } scope :not_blocked_by, ->(account) { where.not(accounts: { id: account.blocking }) } @@ -39,4 +41,12 @@ class CollectionItem < ApplicationRecord def local_item_with_remote_account? local? && account&.remote? end + + private + + def set_position + return if position_changed? + + self.position = self.class.where(collection_id:).maximum(:position).to_i + 1 + end end diff --git a/app/models/concerns/account/interaction_policy_concern.rb b/app/models/concerns/account/interaction_policy_concern.rb new file mode 100644 index 00000000000000..8fe9eda1baf252 --- /dev/null +++ b/app/models/concerns/account/interaction_policy_concern.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +module Account::InteractionPolicyConcern + extend ActiveSupport::Concern + + included do + composed_of :feature_interaction_policy, class_name: 'InteractionPolicy', mapping: { feature_approval_policy: :bitmap } + end + + def feature_policy_as_keys(kind) + raise ArgumentError unless kind.in?(%i(automatic manual)) + return local_feature_policy(kind) if local? + + sub_policy = feature_interaction_policy.send(kind) + sub_policy.as_keys + end + + # Returns `:automatic`, `:manual`, `:unknown`, ':missing` or `:denied` + def feature_policy_for_account(other_account) + return :denied if other_account.nil? || (local? && !discoverable?) + return :automatic if local? + # Post author is always allowed to feature themselves + return :automatic if self == other_account + return :missing if feature_approval_policy.zero? + + automatic_policy = feature_interaction_policy.automatic + following_self = nil + followed_by_self = nil + + return :automatic if automatic_policy.public? + + if automatic_policy.followers? + following_self = followed_by?(other_account) + return :automatic if following_self + end + + if automatic_policy.following? + followed_by_self = following?(other_account) + return :automatic if followed_by_self + end + + # We don't know we are allowed by the automatic policy, considering the manual one + manual_policy = feature_interaction_policy.manual + + return :manual if manual_policy.public? + + if manual_policy.followers? + following_self = followed_by?(other_account) if following_self.nil? + return :manual if following_self + end + + if manual_policy.following? + followed_by_self = following?(other_account) if followed_by_self.nil? + return :manual if followed_by_self + end + + return :unknown if [automatic_policy, manual_policy].any?(&:unsupported_policy?) + + :denied + end + + private + + def local_feature_policy(kind) + return [] if kind == :manual || !discoverable? + + [:public] + end +end diff --git a/app/models/concerns/account/interactions.rb b/app/models/concerns/account/interactions.rb index 525ad0a516e56f..249a56f4f96bb0 100644 --- a/app/models/concerns/account/interactions.rb +++ b/app/models/concerns/account/interactions.rb @@ -123,7 +123,11 @@ def unblock_domain!(other_domain) end def following?(other_account) - active_relationships.exists?(target_account: other_account) + other_id = other_account.is_a?(Account) ? other_account.id : other_account + + preloaded_relation(:following, other_id) do + active_relationships.exists?(target_account: other_account) + end end def following_or_self?(other_account) @@ -156,15 +160,33 @@ def mutual?(other_account) end def blocking?(other_account) - block_relationships.exists?(target_account: other_account) + other_id = other_account.is_a?(Account) ? other_account.id : other_account + + preloaded_relation(:blocking, other_id) do + block_relationships.exists?(target_account: other_account) + end + end + + def blocked_by?(other_account) + other_id = other_account.is_a?(Account) ? other_account.id : other_account + + preloaded_relation(:blocked_by, other_id) do + other_account.block_relationships.exists?(target_account: self) + end end def domain_blocking?(other_domain) - domain_blocks.exists?(domain: other_domain) + preloaded_relation(:domain_blocking_by_domain, other_domain) do + domain_blocks.exists?(domain: other_domain) + end end def muting?(other_account) - mute_relationships.exists?(target_account: other_account) + other_id = other_account.is_a?(Account) ? other_account.id : other_account + + preloaded_relation(:muting, other_id) do + mute_relationships.exists?(target_account: other_account) + end end def muting_conversation?(conversation) @@ -259,4 +281,10 @@ def mutuals def normalized_domain(domain) TagManager.instance.normalize_domain(domain) end + + private + + def preloaded_relation(type, key) + @preloaded_relations && @preloaded_relations[type] ? @preloaded_relations[type][key].present? : yield + end end diff --git a/app/models/concerns/account/mappings.rb b/app/models/concerns/account/mappings.rb index c4eddc1fc22129..b8b43cad7c1601 100644 --- a/app/models/concerns/account/mappings.rb +++ b/app/models/concerns/account/mappings.rb @@ -91,6 +91,12 @@ def build_mapping(query, field) end end + def preload_relations!(...) + @preloaded_relations = relations_map(...) + end + + private + def relations_map(account_ids, domains = nil, **options) relations = { blocked_by: Account.blocked_by_map(account_ids, id), diff --git a/app/models/concerns/status/interaction_policy_concern.rb b/app/models/concerns/status/interaction_policy_concern.rb index 7de73986c3f7cb..f145016be4e6b0 100644 --- a/app/models/concerns/status/interaction_policy_concern.rb +++ b/app/models/concerns/status/interaction_policy_concern.rb @@ -3,32 +3,23 @@ module Status::InteractionPolicyConcern extend ActiveSupport::Concern - QUOTE_APPROVAL_POLICY_FLAGS = { - unsupported_policy: (1 << 0), - public: (1 << 1), - followers: (1 << 2), - following: (1 << 3), - }.freeze - included do + composed_of :quote_interaction_policy, class_name: 'InteractionPolicy', mapping: { quote_approval_policy: :bitmap } + before_validation :downgrade_quote_policy, if: -> { local? && !distributable? } end def quote_policy_as_keys(kind) - return 'public' if Setting.auto_accept_legacy_quotes && kind == :automatic && InstanceInfo.legacy_quote_software?(account.domain) + raise ArgumentError unless kind.in?(%i(automatic manual)) - case kind - when :automatic - policy = quote_approval_policy >> 16 - when :manual - policy = quote_approval_policy & 0xFFFF - end + return 'public' if Setting.auto_accept_legacy_quotes && kind == :automatic && InstanceInfo.legacy_quote_software?(account.domain) - QUOTE_APPROVAL_POLICY_FLAGS.keys.select { |key| policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[key]) }.map(&:to_s) + sub_policy = quote_interaction_policy.send(kind) + sub_policy.as_keys end # Returns `:automatic`, `:manual`, `:unknown` or `:denied` - def quote_policy_for_account(other_account, preloaded_relations: {}) + def quote_policy_for_account(other_account) return :denied if other_account.nil? || direct_visibility? || limited_visibility? || reblog? following_author = nil @@ -36,38 +27,38 @@ def quote_policy_for_account(other_account, preloaded_relations: {}) # Post author is always allowed to quote themselves return :automatic if account_id == other_account.id - return :automatic if Setting.auto_accept_legacy_quotes && !account.local? && InstanceInfo.legacy_quote_software?(account.domain) - automatic_policy = quote_approval_policy >> 16 - manual_policy = quote_approval_policy & 0xFFFF + automatic_policy = quote_interaction_policy.automatic - return :automatic if automatic_policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:public]) + return :automatic if automatic_policy.public? - if automatic_policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:followers]) - following_author = preloaded_relations[:following] ? preloaded_relations[:following][account_id] : other_account.following?(account) if following_author.nil? + if automatic_policy.followers? + following_author = other_account.following?(account) if following_author.nil? return :automatic if following_author end - if automatic_policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:following]) + if automatic_policy.following? followed_by_author = account.following?(other_account) if followed_by_author.nil? return :automatic if followed_by_author end # We don't know we are allowed by the automatic policy, considering the manual one - return :manual if manual_policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:public]) + manual_policy = quote_interaction_policy.manual + + return :manual if manual_policy.public? - if manual_policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:followers]) - following_author = preloaded_relations[:following] ? preloaded_relations[:following][account_id] : other_account.following?(account) if following_author.nil? + if manual_policy.followers? + following_author = other_account.following?(account) if following_author.nil? return :manual if following_author end - if manual_policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:following]) + if manual_policy.following? followed_by_author = account.following?(other_account) if followed_by_author.nil? return :manual if followed_by_author end - return :unknown if (automatic_policy | manual_policy).anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:unsupported_policy]) + return :unknown if [automatic_policy, manual_policy].any?(&:unsupported_policy?) :denied end diff --git a/app/models/concerns/status/threading_concern.rb b/app/models/concerns/status/threading_concern.rb index 9424d66f5e7f2b..94c99309afa875 100644 --- a/app/models/concerns/status/threading_concern.rb +++ b/app/models/concerns/status/threading_concern.rb @@ -8,9 +8,10 @@ def permitted_statuses_from_ids(ids, account, stable: false) statuses = Status.with_accounts(ids).to_a account_ids = statuses.map(&:account_id).uniq domains = statuses.filter_map(&:account_domain).uniq - relations = account&.relations_map(account_ids, domains) || {} - statuses.reject! { |status| StatusFilter.new(status, account, relations).filtered? } + account&.preload_relations!(account_ids, domains) + + statuses.reject! { |status| StatusFilter.new(status, account).filtered? } if stable statuses.sort_by! { |status| ids.index(status.id) } @@ -28,13 +29,8 @@ def descendants(limit, account = nil, depth = nil) find_statuses_from_tree_path(descendant_ids(limit, depth), account, promote: true) end - def readable_references(account = nil) - statuses = references.to_a - account_ids = statuses.map(&:account_id).uniq - domains = statuses.filter_map(&:account_domain).uniq - relations = account&.relations_map(account_ids, domains) || {} - statuses.reject! { |status| StatusFilter.new(status, account, relations).filtered? } - statuses + def readable_references(limit, account = nil) + find_statuses_from_tree_path(referred_by_ids(limit), account) end def self_replies(limit) @@ -43,6 +39,10 @@ def self_replies(limit) private + def referred_by_ids(limit) + references.reorder(id: :desc).limit(limit) + end + def ancestor_ids(limit) key = "ancestors:#{id}" ancestors = Rails.cache.fetch(key) diff --git a/app/models/concerns/user/has_settings.rb b/app/models/concerns/user/has_settings.rb index 31983b720eebd6..44ecdedb34100c 100644 --- a/app/models/concerns/user/has_settings.rb +++ b/app/models/concerns/user/has_settings.rb @@ -163,6 +163,14 @@ def setting_discoverable_local settings['discoverable_local'] end + def setting_color_scheme + settings['web.color_scheme'] + end + + def setting_contrast + settings['web.contrast'] + end + def setting_display_media settings['web.display_media'] end diff --git a/app/models/domain_allow.rb b/app/models/domain_allow.rb index 8eab3164e5b2b3..783269bf258831 100644 --- a/app/models/domain_allow.rb +++ b/app/models/domain_allow.rb @@ -33,7 +33,7 @@ def allowed_domains def rule_for(domain) return if domain.blank? - uri = Addressable::URI.new.tap { |u| u.host = domain.delete('/') } + uri = Addressable::URI.new.tap { |u| u.host = domain.strip.delete('/') } find_by(domain: uri.normalized_host) end diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index b0cd5621c4a2e9..c2f34bbdefa1fb 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -71,6 +71,7 @@ class Form::AdminSettings local_topic_feed_access remote_topic_feed_access landing_page + wrapstodon ).freeze INTEGER_KEYS = %i( @@ -115,6 +116,7 @@ class Form::AdminSettings delete_content_cache_without_reaction hold_remote_new_accounts auto_accept_legacy_quotes + wrapstodon ).freeze UPLOAD_KEYS = %i( @@ -210,6 +212,10 @@ def save end end + def persisted? + true + end + private def cache_digest_value(key) diff --git a/app/models/generated_annual_report.rb b/app/models/generated_annual_report.rb index 563dd9219cd486..60c7fe40b122a9 100644 --- a/app/models/generated_annual_report.rb +++ b/app/models/generated_annual_report.rb @@ -33,7 +33,7 @@ def account_ids when 1 data['most_reblogged_accounts'].pluck('account_id') + data['commonly_interacted_with_accounts'].pluck('account_id') when 2 - [] + [account_id] end end diff --git a/app/models/user_settings.rb b/app/models/user_settings.rb index 5e1ca26be45238..36ca1aa2bf9893 100644 --- a/app/models/user_settings.rb +++ b/app/models/user_settings.rb @@ -83,6 +83,8 @@ class KeyError < Error; end setting :hide_emoji_reaction_count, default: false setting :show_avatar_on_filter, default: true setting :community_timeline_instead_of_search_menu, default: false + setting :color_scheme, default: 'auto', in: %w(auto light dark) + setting :contrast, default: 'auto', in: %w(auto high) setting_inverse_alias :'web.show_emoji_reaction_count', :'web.hide_emoji_reaction_count' setting_inverse_alias :'web.show_favourite_menu', :'web.hide_favourite_menu' diff --git a/app/policies/account_policy.rb b/app/policies/account_policy.rb index dd9d043e015fbe..caa7d9831b5486 100644 --- a/app/policies/account_policy.rb +++ b/app/policies/account_policy.rb @@ -72,4 +72,8 @@ def approve_remote? def reject_remote? role.can?(:manage_users) && record.remote_pending end + + def feature? + record.featureable? && !current_account.blocking?(record) && !current_account.blocked_by?(record) + end end diff --git a/app/policies/admin/status_policy.rb b/app/policies/admin/status_policy.rb index d320b98da80c6c..5f3ba6c99d66b1 100644 --- a/app/policies/admin/status_policy.rb +++ b/app/policies/admin/status_policy.rb @@ -1,12 +1,6 @@ # frozen_string_literal: true class Admin::StatusPolicy < ApplicationPolicy - def initialize(current_account, record, preloaded_relations = {}) - super(current_account, record) - - @preloaded_relations = preloaded_relations - end - def index? role.can?(:manage_reports, :manage_users) end @@ -34,6 +28,6 @@ def eligible_to_show? end def viewable_through_normal_policy? - StatusPolicy.new(current_account, record, @preloaded_relations).show? + StatusPolicy.new(current_account, record).show? end end diff --git a/app/policies/status_policy.rb b/app/policies/status_policy.rb index 1ae1c4731f91c4..0ff63f044c71ca 100644 --- a/app/policies/status_policy.rb +++ b/app/policies/status_policy.rb @@ -1,15 +1,6 @@ # frozen_string_literal: true class StatusPolicy < ApplicationPolicy - def initialize(current_account, record, preloaded_relations = {}, preloaded_status_relations = {}) - super(current_account, record) - - @preloaded_relations = preloaded_relations - @preloaded_status_relations = preloaded_status_relations - end - - delegate :reply?, :expired?, to: :record - def show? return false if author.unavailable? @@ -37,7 +28,7 @@ def show_activity? # This is about requesting a quote post, not validating it def quote? - show? && record.quote_policy_for_account(current_account, preloaded_relations: @preloaded_relations) != :denied + show? && record.quote_policy_for_account(current_account) != :denied end def reblog? @@ -108,19 +99,19 @@ def author_blocking_domain? def blocking_author? return false if current_account.nil? - @preloaded_relations[:blocking] ? @preloaded_relations[:blocking][author.id] : current_account.blocking?(author) + current_account.blocking?(author) end def author_blocking? return false if current_account.nil? - @preloaded_relations[:blocked_by] ? @preloaded_relations[:blocked_by][author.id] : author.blocking?(current_account) + current_account.blocked_by?(author) end def following_author? return false if current_account.nil? - @preloaded_relations[:following] ? @preloaded_relations[:following][author.id] : current_account.following?(author) + current_account.following?(author) end def following_author_domain? diff --git a/app/presenters/status_relationships_presenter.rb b/app/presenters/status_relationships_presenter.rb index 7c395c6000a1e3..5e9a6e5d6476f2 100644 --- a/app/presenters/status_relationships_presenter.rb +++ b/app/presenters/status_relationships_presenter.rb @@ -43,14 +43,8 @@ def initialize(statuses, current_account_id = nil, **options) end # This one is currently on-demand as it is only used for quote posts - def preloaded_account_relations - @preloaded_account_relations ||= begin - accounts = @statuses.compact.flat_map { |s| [s.account, s.proper.account, s.proper.quote&.quoted_account] }.uniq.compact - - account_ids = accounts.pluck(:id) - account_domains = accounts.pluck(:domain).uniq - Account.find(@current_account_id).relations_map(account_ids, account_domains) - end + def authoring_accounts + @authoring_accounts ||= @statuses.compact.flat_map { |s| [s.account, s.proper.account, s.proper.quote&.quoted_account] }.uniq.compact end private diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index 0c04511ce8c75e..dbf0ba8a608d5c 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -10,12 +10,16 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer :moved_to, :property_value, :discoverable, :suspended, :searchable_by, :other_setting, :memorial, :indexable, :attribution_domains + context_extensions :interaction_policies if Mastodon::Feature.collections_enabled? + attributes :id, :type, :following, :followers, :inbox, :outbox, :featured, :featured_tags, :preferred_username, :name, :summary, :url, :manually_approves_followers, :discoverable, :indexable, :published, :memorial, :searchable_by, :other_setting + attribute :interaction_policy, if: -> { Mastodon::Feature.collections_enabled? } + has_one :public_key, serializer: ActivityPub::PublicKeySerializer has_many :virtual_tags, key: :tag @@ -186,6 +190,16 @@ def other_setting end end + def interaction_policy + uri = object.discoverable? ? ActivityPub::TagManager::COLLECTIONS[:public] : ActivityPub::TagManager.instance.uri_for(object) + + { + canFeature: { + automaticApproval: [uri], + }, + } + end + class CustomEmojiSerializer < ActivityPub::EmojiSerializer end diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index 4fcc38bc7dd32d..540b2219729c8a 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -302,10 +302,10 @@ def interaction_policy approved_uris = [] # On outgoing posts, only automatic approval is supported - policy = object.quote_approval_policy >> 16 - approved_uris << ActivityPub::TagManager::COLLECTIONS[:public] if policy.anybits?(Status::QUOTE_APPROVAL_POLICY_FLAGS[:public]) - approved_uris << ActivityPub::TagManager.instance.followers_uri_for(object.account) if policy.anybits?(Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers]) - approved_uris << ActivityPub::TagManager.instance.following_uri_for(object.account) if policy.anybits?(Status::QUOTE_APPROVAL_POLICY_FLAGS[:following]) + policy = object.quote_interaction_policy.automatic + approved_uris << ActivityPub::TagManager::COLLECTIONS[:public] if policy.public? + approved_uris << ActivityPub::TagManager.instance.followers_uri_for(object.account) if policy.followers? + approved_uris << ActivityPub::TagManager.instance.following_uri_for(object.account) if policy.following? approved_uris << ActivityPub::TagManager.instance.uri_for(object.account) if approved_uris.empty? { diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index e245186b54d1e6..235d2b511b025e 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -49,8 +49,9 @@ def meta object_account_user.setting_show_avatar_on_filter ? nil : 'avatar_on_filter', ].compact store[:enabled_visibilities] = enabled_visibilities - store[:featured_tags] = object.current_account.featured_tags.pluck(:name) - store[:emoji_style] = object_account_user.settings['web.emoji_style'] + store[:featured_tags] = object.current_account.featured_tags.pluck(:name) + store[:emoji_style] = object_account_user.settings['web.emoji_style'] + store[:wrapstodon] = wrapstodon else store[:auto_play_gif] = Setting.auto_play_gif store[:display_media] = Setting.display_media @@ -126,6 +127,16 @@ def features private + def wrapstodon + current_campaign = AnnualReport.current_campaign + return if current_campaign.blank? + + { + year: current_campaign, + state: AnnualReport.new(object.current_account, current_campaign).state, + } + end + def default_meta_store { access_token: object.token, diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb index 68ff439cec38b3..645c683a5306e9 100644 --- a/app/serializers/rest/account_serializer.rb +++ b/app/serializers/rest/account_serializer.rb @@ -20,6 +20,8 @@ class REST::AccountSerializer < ActiveModel::Serializer attribute :memorial, if: :memorial? + attribute :feature_approval, if: -> { Mastodon::Feature.collections_enabled? } + class AccountDecorator < SimpleDelegator def self.model_name Account.model_name @@ -170,19 +172,15 @@ def moved_and_not_nested? object.moved? end - def statuses_count - object.public_statuses_count - end - - def followers_count - object.public_followers_count - end - - def following_count - object.public_following_count - end - def other_settings object.suspended? ? {} : object.public_settings_for_local end + + def feature_approval + { + automatic: object.feature_policy_as_keys(:automatic), + manual: object.feature_policy_as_keys(:manual), + current_user: object.feature_policy_for_account(current_user&.account), + } + end end diff --git a/app/serializers/rest/annual_report_serializer.rb b/app/serializers/rest/annual_report_serializer.rb index 99c313e6cb360c..85a9c04540fcf0 100644 --- a/app/serializers/rest/annual_report_serializer.rb +++ b/app/serializers/rest/annual_report_serializer.rb @@ -3,9 +3,13 @@ class REST::AnnualReportSerializer < ActiveModel::Serializer include RoutingHelper - attributes :year, :data, :schema_version, :share_url + attributes :year, :data, :schema_version, :share_url, :account_id def share_url public_wrapstodon_url(object.account, object.year, object.share_key) if object.share_key.present? end + + def account_id + object.account_id.to_s + end end diff --git a/app/serializers/rest/base_collection_serializer.rb b/app/serializers/rest/base_collection_serializer.rb index 0e9bfc4cfc6580..be26aac6fe21a2 100644 --- a/app/serializers/rest/base_collection_serializer.rb +++ b/app/serializers/rest/base_collection_serializer.rb @@ -9,8 +9,4 @@ class REST::BaseCollectionSerializer < ActiveModel::Serializer def id object.id.to_s end - - def item_count - object.respond_to?(:item_count) ? object.item_count : object.collection_items.count - end end diff --git a/app/serializers/rest/base_quote_serializer.rb b/app/serializers/rest/base_quote_serializer.rb index ac3b545d5c36ba..407bc961f6f1d5 100644 --- a/app/serializers/rest/base_quote_serializer.rb +++ b/app/serializers/rest/base_quote_serializer.rb @@ -19,6 +19,13 @@ def quoted_status private def status_filter - @status_filter ||= StatusFilter.new(object.quoted_status, current_user&.account, instance_options[:relationships]&.preloaded_account_relations || {}) + @status_filter ||= begin + if current_user && instance_options[:relationships] + account_ids = instance_options[:relationships].authoring_accounts.pluck(:id) + domains = instance_options[:relationships].authoring_accounts.pluck(:domain).uniq + current_user.account.preload_relations!(account_ids, domains) + end + StatusFilter.new(object.quoted_status, current_user&.account) + end end end diff --git a/app/serializers/rest/collection_item_serializer.rb b/app/serializers/rest/collection_item_serializer.rb index c0acc87bfd4e02..d35a8fdef28622 100644 --- a/app/serializers/rest/collection_item_serializer.rb +++ b/app/serializers/rest/collection_item_serializer.rb @@ -3,7 +3,11 @@ class REST::CollectionItemSerializer < ActiveModel::Serializer delegate :accepted?, to: :object - attributes :position, :state + attributes :id, :position, :state belongs_to :account, serializer: REST::AccountSerializer, if: :accepted? + + def id + object.id.to_s + end end diff --git a/app/serializers/rest/scheduled_status_serializer.rb b/app/serializers/rest/scheduled_status_serializer.rb index 71ddb7b3e1707b..cc66c7cc95f218 100644 --- a/app/serializers/rest/scheduled_status_serializer.rb +++ b/app/serializers/rest/scheduled_status_serializer.rb @@ -12,7 +12,7 @@ def id def params object.params.merge( quoted_status_id: object.params['quoted_status_id']&.to_s, - quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS.keys.find { |key| object.params['quote_approval_policy']&.anybits?(Status::QUOTE_APPROVAL_POLICY_FLAGS[key] << 16) }&.to_s || 'nobody' + quote_approval_policy: InteractionPolicy::POLICY_FLAGS.keys.find { |key| object.params['quote_approval_policy']&.anybits?(InteractionPolicy::POLICY_FLAGS[key] << 16) }&.to_s || 'nobody' ) end end diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index 3837a3eb96369f..8a266c1824bd40 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -124,6 +124,7 @@ def set_immediate_protocol_attributes! @account.uri = @uri @account.actor_type = actor_type @account.created_at = @json['published'] if @json['published'].present? + @account.feature_approval_policy = feature_approval_policy if Mastodon::Feature.collections_enabled? end def valid_collection_uri(uri) @@ -498,4 +499,8 @@ def process_emoji(tag) emoji.image_remote_url = image_url emoji.save end + + def feature_approval_policy + ActivityPub::Parser::InteractionPolicyParser.new(@json.dig('interactionPolicy', 'canFeature'), @account).bitmap + end end diff --git a/app/services/add_account_to_collection_service.rb b/app/services/add_account_to_collection_service.rb new file mode 100644 index 00000000000000..4477384dd087c7 --- /dev/null +++ b/app/services/add_account_to_collection_service.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class AddAccountToCollectionService + def call(collection, account) + raise ArgumentError unless collection.local? + + @collection = collection + @account = account + + raise Mastodon::NotPermittedError, I18n.t('accounts.errors.cannot_be_added_to_collections') unless AccountPolicy.new(@collection.account, @account).feature? + + create_collection_item + end + + private + + def create_collection_item + @collection.collection_items.create!( + account: @account, + state: :accepted + ) + end +end diff --git a/app/services/create_collection_service.rb b/app/services/create_collection_service.rb index 92c26879d19b83..10843cb9677170 100644 --- a/app/services/create_collection_service.rb +++ b/app/services/create_collection_service.rb @@ -2,9 +2,10 @@ class CreateCollectionService def call(params, account) - account_ids = params.delete(:account_ids) + @account = account + @accounts_to_add = Account.find(params.delete(:account_ids) || []) @collection = Collection.new(params.merge({ account:, local: true })) - build_items(account_ids) + build_items @collection.save! @collection @@ -12,13 +13,14 @@ def call(params, account) private - def build_items(account_ids) - return if account_ids.blank? + def build_items + return if @accounts_to_add.empty? - account_ids.each do |account_id| - account = Account.find(account_id) - # TODO: validate preferences - @collection.collection_items.build(account:) + @account.preload_relations!(@accounts_to_add.map(&:id)) + @accounts_to_add.each do |account_to_add| + raise Mastodon::NotPermittedError, I18n.t('accounts.errors.cannot_be_added_to_collections') unless AccountPolicy.new(@account, account_to_add).feature? + + @collection.collection_items.build(account: account_to_add) end end end diff --git a/app/services/process_mentions_service.rb b/app/services/process_mentions_service.rb index a8d95a53d70d45..f7084b126f7e53 100644 --- a/app/services/process_mentions_service.rb +++ b/app/services/process_mentions_service.rb @@ -80,7 +80,7 @@ def assign_mentions! # Make sure we never mention blocked accounts unless @current_mentions.empty? mentioned_domains = @current_mentions.filter_map { |m| m.account.domain }.uniq - blocked_domains = Set.new(mentioned_domains.empty? ? [] : AccountDomainBlock.where(account_id: @status.account_id, domain: mentioned_domains)) + blocked_domains = Set.new(mentioned_domains.empty? ? [] : AccountDomainBlock.where(account_id: @status.account_id, domain: mentioned_domains).pluck(:domain)) mentioned_account_ids = @current_mentions.map(&:account_id) blocked_account_ids = Set.new(@status.account.block_relationships.where(target_account_id: mentioned_account_ids).pluck(:target_account_id)) diff --git a/app/services/statuses_search_service.rb b/app/services/statuses_search_service.rb index 4a18d073845ee1..568fd47d3540cd 100644 --- a/app/services/statuses_search_service.rb +++ b/app/services/statuses_search_service.rb @@ -33,9 +33,10 @@ def status_search_results results = request.collapse(field: :id).order(id: { order: query.order_by }).limit(@limit).offset(@offset).objects.compact account_ids = results.map(&:account_id) account_domains = results.map(&:account_domain) - preloaded_relations = @account.relations_map(account_ids, account_domains) - results.reject { |status| StatusFilter.new(status, @account, preloaded_relations).filtered? } + @account.preload_relations!(account_ids, account_domains) + + results.reject { |status| StatusFilter.new(status, @account).filtered? } rescue Faraday::ConnectionFailed, Parslet::ParseFailed, Errno::ENETUNREACH [] end diff --git a/app/views/admin/settings/about/show.html.haml b/app/views/admin/settings/about/show.html.haml index adc8f1ff04ac36..c4df98e7041091 100644 --- a/app/views/admin/settings/about/show.html.haml +++ b/app/views/admin/settings/about/show.html.haml @@ -5,7 +5,7 @@ %h2= t('admin.settings.title') = render partial: 'admin/settings/shared/links' -= simple_form_for @admin_settings, url: admin_settings_about_path, html: { method: :patch } do |f| += simple_form_for @admin_settings, url: admin_settings_about_path do |f| = render 'shared/error_messages', object: @admin_settings %p.lead= t('admin.settings.about.preamble') diff --git a/app/views/admin/settings/appearance/show.html.haml b/app/views/admin/settings/appearance/show.html.haml index c610f9f94174c3..20208df15248f1 100644 --- a/app/views/admin/settings/appearance/show.html.haml +++ b/app/views/admin/settings/appearance/show.html.haml @@ -5,7 +5,7 @@ %h2= t('admin.settings.title') = render partial: 'admin/settings/shared/links' -= simple_form_for @admin_settings, url: admin_settings_appearance_path, html: { method: :patch } do |f| += simple_form_for @admin_settings, url: admin_settings_appearance_path do |f| = render 'shared/error_messages', object: @admin_settings %p.lead= t('admin.settings.appearance.preamble') diff --git a/app/views/admin/settings/branding/show.html.haml b/app/views/admin/settings/branding/show.html.haml index 05795a19745281..22116167eb36b8 100644 --- a/app/views/admin/settings/branding/show.html.haml +++ b/app/views/admin/settings/branding/show.html.haml @@ -5,7 +5,7 @@ %h2= t('admin.settings.title') = render partial: 'admin/settings/shared/links' -= simple_form_for @admin_settings, url: admin_settings_branding_path, html: { method: :patch } do |f| += simple_form_for @admin_settings, url: admin_settings_branding_path do |f| = render 'shared/error_messages', object: @admin_settings %p.lead= t('admin.settings.branding.preamble') diff --git a/app/views/admin/settings/content_retention/show.html.haml b/app/views/admin/settings/content_retention/show.html.haml index 5e7ebfbdd9e7b9..873728f0c88cd0 100644 --- a/app/views/admin/settings/content_retention/show.html.haml +++ b/app/views/admin/settings/content_retention/show.html.haml @@ -5,7 +5,7 @@ %h2= t('admin.settings.title') = render partial: 'admin/settings/shared/links' -= simple_form_for @admin_settings, url: admin_settings_content_retention_path, html: { method: :patch } do |f| += simple_form_for @admin_settings, url: admin_settings_content_retention_path do |f| = render 'shared/error_messages', object: @admin_settings %p.lead= t('admin.settings.content_retention.preamble') diff --git a/app/views/admin/settings/discovery/show.html.haml b/app/views/admin/settings/discovery/show.html.haml index fd9b3d5a922a6f..30f76bde9e8030 100644 --- a/app/views/admin/settings/discovery/show.html.haml +++ b/app/views/admin/settings/discovery/show.html.haml @@ -5,7 +5,7 @@ %h2= t('admin.settings.title') = render partial: 'admin/settings/shared/links' -= simple_form_for @admin_settings, url: admin_settings_discovery_path, html: { method: :patch } do |f| += simple_form_for @admin_settings, url: admin_settings_discovery_path do |f| = render 'shared/error_messages', object: @admin_settings %p.lead= t('admin.settings.discovery.preamble') @@ -146,5 +146,12 @@ as: :boolean, wrapper: :with_label + %h4= t('admin.settings.discovery.wrapstodon') + + .fields-group + = f.input :wrapstodon, + as: :boolean, + wrapper: :with_label + .actions = f.button :button, t('generic.save_changes'), type: :submit diff --git a/app/views/admin/settings/registrations/show.html.haml b/app/views/admin/settings/registrations/show.html.haml index a9056ca0108b6c..1ba5629f9734a0 100644 --- a/app/views/admin/settings/registrations/show.html.haml +++ b/app/views/admin/settings/registrations/show.html.haml @@ -5,7 +5,7 @@ %h2= t('admin.settings.title') = render partial: 'admin/settings/shared/links' -= simple_form_for @admin_settings, url: admin_settings_registrations_path, html: { method: :patch } do |f| += simple_form_for @admin_settings, url: admin_settings_registrations_path do |f| = render 'shared/error_messages', object: @admin_settings %p.lead= t('admin.settings.registrations.preamble') diff --git a/app/views/filters/_filter.html.haml b/app/views/filters/_filter.html.haml index 15326f300626e8..bf2f4dd7d1d736 100644 --- a/app/views/filters/_filter.html.haml +++ b/app/views/filters/_filter.html.haml @@ -19,9 +19,7 @@ .permissions-list__item__text__title = t('filters.index.keywords', count: filter.keywords.size) .permissions-list__item__text__type - - keywords = filter.keywords.map(&:keyword) - - keywords = keywords.take(5) + ['…'] if keywords.size > 5 # TODO - = keywords.join(', ') + = filter_keywords(filter) - unless filter.statuses.empty? %li.permissions-list__item .permissions-list__item__icon diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 17893e7232f830..4c2b4ff5d137a2 100755 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -1,5 +1,5 @@ !!! 5 -%html{ lang: I18n.locale, class: html_classes, 'data-user-theme': current_theme.parameterize } +%html{ lang: I18n.locale, class: html_classes, 'data-user-theme': current_theme.parameterize, 'data-contrast': contrast.parameterize, 'data-mode': color_scheme.parameterize } %head %meta{ charset: 'utf-8' }/ %meta{ name: 'viewport', content: 'width=device-width, initial-scale=1, viewport-fit=cover' }/ diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml index ad9e57972afaba..3dca6a2d4a7525 100644 --- a/app/views/settings/preferences/appearance/show.html.haml +++ b/app/views/settings/preferences/appearance/show.html.haml @@ -29,7 +29,27 @@ include_blank: false, label_method: ->(theme) { I18n.t("themes.#{theme}", default: theme) }, label: I18n.t('simple_form.labels.defaults.setting_theme'), - wrapper: :with_label + wrapper: :with_label, + required: false + - if Mastodon::Feature.new_theme_options_enabled? + .input.horizontal-options + = ff.input :'web.color_scheme', + as: :radio_buttons, + collection: %w(auto light dark), + include_blank: false, + label: I18n.t('simple_form.labels.defaults.setting_color_scheme'), + label_method: ->(contrast) { I18n.t("color_scheme.#{contrast}", default: contrast) }, + wrapper: :with_label, + required: false + .input.horizontal-options + = ff.input :'web.contrast', + as: :radio_buttons, + collection: %w(auto high), + include_blank: false, + label: I18n.t('simple_form.labels.defaults.setting_contrast'), + label_method: ->(contrast) { I18n.t("contrast.#{contrast}", default: contrast) }, + wrapper: :with_label, + required: false .fields-group = f.simple_fields_for :settings, current_user.settings do |ff| @@ -39,11 +59,13 @@ hint: I18n.t('simple_form.hints.defaults.setting_emoji_style'), label: I18n.t('simple_form.labels.defaults.setting_emoji_style'), label_method: ->(emoji_style) { I18n.t("emoji_styles.#{emoji_style}", default: emoji_style) }, - wrapper: :with_label + wrapper: :with_label, + required: false - unless I18n.locale == :en .flash-message.translation-prompt - #{t 'appearance.localization.body'} #{content_tag(:a, t('appearance.localization.guide_link_text'), href: t('appearance.localization.guide_link'), target: '_blank', rel: 'noopener')} + = t 'appearance.localization.body' + = link_to t('appearance.localization.guide_link_text'), t('appearance.localization.guide_link'), target: '_blank', rel: 'noopener' = f.simple_fields_for :settings, current_user.settings do |ff| %h4= t 'appearance.animations_and_accessibility' @@ -126,7 +148,8 @@ item_wrapper_tag: 'li', label_method: ->(item) { t("simple_form.hints.defaults.setting_display_media_#{item}") }, label: I18n.t('simple_form.labels.defaults.setting_display_media'), - wrapper: :with_floating_label + wrapper: :with_floating_label, + required: false .fields-group = ff.input :'web.use_blurhash', diff --git a/app/views/settings/preferences/notifications/show.html.haml b/app/views/settings/preferences/notifications/show.html.haml index 81847575110f03..febcee752ba4fa 100644 --- a/app/views/settings/preferences/notifications/show.html.haml +++ b/app/views/settings/preferences/notifications/show.html.haml @@ -41,4 +41,5 @@ include_blank: false, label_method: ->(setting) { I18n.t("simple_form.labels.notification_emails.software_updates.#{setting}") }, label: I18n.t('simple_form.labels.notification_emails.software_updates.label'), - wrapper: :with_label + wrapper: :with_label, + required: false diff --git a/app/views/wrapstodon/_og_description.html.haml b/app/views/wrapstodon/_og_description.html.haml new file mode 100644 index 00000000000000..7b6e04cb8ee86b --- /dev/null +++ b/app/views/wrapstodon/_og_description.html.haml @@ -0,0 +1,4 @@ +- description = t('wrapstodon.description', name: display_name(account)) + +%meta{ name: 'description', content: description }/ += opengraph 'og:description', description diff --git a/app/views/wrapstodon/_og_image.html.haml b/app/views/wrapstodon/_og_image.html.haml new file mode 100644 index 00000000000000..8a52a568ac345a --- /dev/null +++ b/app/views/wrapstodon/_og_image.html.haml @@ -0,0 +1,6 @@ +- if %w(lurker booster pollster replier oracle).include?(report.data['archetype']) + = opengraph 'og:image', frontend_asset_url("images/archetypes/previews/#{report.data['archetype']}.jpg") + = opengraph 'og:image:type', 'image/jpeg' + = opengraph 'og:image:width', 1200 + = opengraph 'og:image:height', 630 + = opengraph 'twitter:card', 'summary_large_image' diff --git a/app/views/wrapstodon/show.html.haml b/app/views/wrapstodon/show.html.haml index 70c8c77b19c500..ed1e64d4665298 100644 --- a/app/views/wrapstodon/show.html.haml +++ b/app/views/wrapstodon/show.html.haml @@ -4,9 +4,16 @@ %meta{ name: 'robots', content: 'noindex, noarchive' }/ = opengraph 'og:site_name', site_title + = opengraph 'og:type', 'article' + = opengraph 'og:title', t('wrapstodon.title', name: display_name(@account), year: @generated_annual_report.year) = opengraph 'profile:username', acct(@account)[1..] + = render 'og_description', account: @account + = render 'og_image', report: @generated_annual_report + = vite_typescript_tag 'wrapstodon.tsx', crossorigin: 'anonymous' +- content_for :html_classes, 'theme-dark' + #wrapstodon = render_wrapstodon_share_data @generated_annual_report diff --git a/app/workers/activitypub/distribution_worker.rb b/app/workers/activitypub/distribution_worker.rb index 260334760edb82..4d7b21910864fc 100644 --- a/app/workers/activitypub/distribution_worker.rb +++ b/app/workers/activitypub/distribution_worker.rb @@ -1,6 +1,11 @@ # frozen_string_literal: true class ActivityPub::DistributionWorker < ActivityPub::RawDistributionWorker + # Skip followers synchronization for accounts with a large number of followers, + # as this is expensive and people with very large amounts of followers + # necessarily have less control over them to begin with + MAX_FOLLOWERS_FOR_SYNCHRONIZATION = 25_000 + # Distribute a new status or an edit of a status to all the places # where the status is supposed to go or where it was interacted with def perform(status_id) @@ -88,6 +93,6 @@ def always_sign end def options - { 'synchronize_followers' => @status.private_visibility? } + { 'synchronize_followers' => @status.private_visibility? && @account.followers_count < MAX_FOLLOWERS_FOR_SYNCHRONIZATION } end end diff --git a/config/boot.rb b/config/boot.rb index 70ffe22c04d979..29e8e9e349a239 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true unless ENV.key?('RAILS_ENV') - abort <<~ERROR + abort <<~ERROR # rubocop:disable Rails/Exit The RAILS_ENV environment variable is not set. Please set it correctly depending on context: diff --git a/config/initializers/active_record_encryption.rb b/config/initializers/active_record_encryption.rb index 9ae28e401bab4d..3f9125674c3352 100644 --- a/config/initializers/active_record_encryption.rb +++ b/config/initializers/active_record_encryption.rb @@ -13,7 +13,7 @@ value = ENV.fetch(key, '') if value.blank? - abort <<~MESSAGE + abort <<~MESSAGE # rubocop:disable Rails/Exit Mastodon now requires that these variables are set: @@ -28,7 +28,7 @@ next unless Rails.env.production? && value.end_with?('DO_NOT_USE_IN_PRODUCTION') - abort <<~MESSAGE + abort <<~MESSAGE # rubocop:disable Rails/Exit It looks like you are trying to run Mastodon in production with a #{key} value from the test environment. diff --git a/config/initializers/deprecations.rb b/config/initializers/deprecations.rb index 15c701c9c1d77e..520707e59ff415 100644 --- a/config/initializers/deprecations.rb +++ b/config/initializers/deprecations.rb @@ -14,7 +14,7 @@ In addition, as REDIS_NAMESPACE is being used as a prefix for Elasticsearch, please do not forget to set ES_PREFIX to "#{ENV.fetch('REDIS_NAMESPACE')}". MESSAGE - abort message + abort message # rubocop:disable Rails/Exit end if ENV['MASTODON_USE_LIBVIPS'] == 'false' diff --git a/config/initializers/vips.rb b/config/initializers/vips.rb index a539d7035c4e1a..09210d60ebe4f5 100644 --- a/config/initializers/vips.rb +++ b/config/initializers/vips.rb @@ -6,7 +6,7 @@ require 'vips' unless Vips.at_least_libvips?(8, 13) - abort <<~ERROR.squish + abort <<~ERROR.squish # rubocop:disable Rails/Exit Incompatible libvips version (#{Vips.version_string}), please install libvips >= 8.13 ERROR end diff --git a/config/locales/activerecord.ca.yml b/config/locales/activerecord.ca.yml index f53f7f364a0f4e..f92aca17a2e9c5 100644 --- a/config/locales/activerecord.ca.yml +++ b/config/locales/activerecord.ca.yml @@ -32,6 +32,12 @@ ca: attributes: url: invalid: no és un enllaç vàlid + collection: + attributes: + collection_items: + too_many: n'hi ha massa. %{count} és el màxim permès + tag: + unusable: pot no ser emprat doorkeeper/application: attributes: website: diff --git a/config/locales/activerecord.en-GB.yml b/config/locales/activerecord.en-GB.yml index 2402b2f7f253b8..cd94e0942231e2 100644 --- a/config/locales/activerecord.en-GB.yml +++ b/config/locales/activerecord.en-GB.yml @@ -7,7 +7,7 @@ en-GB: options: Choices user: agreement: Service agreement - email: E-mail address + email: Email address locale: Locale password: Password user/account: @@ -26,7 +26,7 @@ en-GB: fields: fields_with_values_missing_labels: contains values with missing labels username: - invalid: must contain only letters, numbers and underscores + invalid: must contain only letters, numbers, and underscores reserved: is reserved admin/webhook: attributes: @@ -64,7 +64,7 @@ en-GB: date_of_birth: below_limit: is below the age limit email: - blocked: uses a disallowed e-mail provider + blocked: uses a disallowed email provider unreachable: does not seem to exist role_id: elevated: cannot be higher than your current role @@ -80,4 +80,4 @@ en-GB: webhook: attributes: events: - invalid_permissions: cannot include events you don't have the rights to + invalid_permissions: cannot include events to which you don't have the rights diff --git a/config/locales/activerecord.eu.yml b/config/locales/activerecord.eu.yml index c0ac1bb338b046..4324106061d9a7 100644 --- a/config/locales/activerecord.eu.yml +++ b/config/locales/activerecord.eu.yml @@ -32,6 +32,12 @@ eu: attributes: url: invalid: ez da baliozko URL bat + collection: + attributes: + collection_items: + too_many: gehiegi dira, eta ez dira onartzen %{count} baino gehiago + tag: + unusable: baliteke ez erabiltzea doorkeeper/application: attributes: website: diff --git a/config/locales/activerecord.ko.yml b/config/locales/activerecord.ko.yml index 3aa991734b832a..05be3155831d08 100644 --- a/config/locales/activerecord.ko.yml +++ b/config/locales/activerecord.ko.yml @@ -32,6 +32,10 @@ ko: attributes: url: invalid: 올바른 URL이 아닙니다 + collection: + attributes: + tag: + unusable: 사용할 수 없음 doorkeeper/application: attributes: website: diff --git a/config/locales/be.yml b/config/locales/be.yml index 3212b7cbb3c9cf..77d515b37c92a8 100644 --- a/config/locales/be.yml +++ b/config/locales/be.yml @@ -7,6 +7,8 @@ be: hosted_on: Mastodon месціцца на %{domain} title: Пра нас accounts: + errors: + cannot_be_added_to_collections: Гэты ўліковы запіс нельга дадаць у калекцыі. followers: few: Падпісчыка many: Падпісчыкаў @@ -874,6 +876,7 @@ be: publish_statistics: Апублікаваць статыстыку title: Выяўленне trends: Трэнды + wrapstodon: Вынікадон domain_blocks: all: Для ўсіх disabled: Нікому @@ -2274,4 +2277,5 @@ be: otp_required: Каб выкарыстоўваць ключы бяспекі, спачатку ўключыце двухфактарную аўтэнтыфікацыю. registered_on: Зарэгістраваны %{date} wrapstodon: + description: Паглядзіце, як %{name} карыстаў(-ла)ся Mastodon у гэтым годзе! title: Вынікадон %{year} для %{name} diff --git a/config/locales/ca.yml b/config/locales/ca.yml index bc7cd7673accf6..baf33daacf2eb0 100644 --- a/config/locales/ca.yml +++ b/config/locales/ca.yml @@ -7,6 +7,8 @@ ca: hosted_on: Mastodon allotjat a %{domain} title: Quant a accounts: + errors: + cannot_be_added_to_collections: Aquest compte no es pot afegir a coŀleccions. followers: one: Seguidor other: Seguidors @@ -2160,4 +2162,5 @@ ca: otp_required: Per a usar claus de seguretat, activeu primer l'autenticació de dos factors. registered_on: Registrat en %{date} wrapstodon: + description: Mira com %{name} ha fet servir Mastodon enguany. title: Wrapstodon %{year} per a %{name} diff --git a/config/locales/cs.yml b/config/locales/cs.yml index 25d61bbb0d9fd6..90f118e82456dc 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -7,6 +7,8 @@ cs: hosted_on: Mastodon na doméně %{domain} title: O službě accounts: + errors: + cannot_be_added_to_collections: Tento účet nelze přidat do kolekcí. followers: few: Sledující many: Sledujících @@ -2274,4 +2276,5 @@ cs: otp_required: Pro použití bezpečnostních klíčů prosím nejprve zapněte dvoufázové ověřování. registered_on: Přidán %{date} wrapstodon: + description: Podívejte se, jak %{name} používali Mastodon v tomto roce! title: Wrapstodon %{year} pro %{name} diff --git a/config/locales/cy.yml b/config/locales/cy.yml index ad153fdac31458..ab45735c671f01 100644 --- a/config/locales/cy.yml +++ b/config/locales/cy.yml @@ -7,6 +7,8 @@ cy: hosted_on: Mastodon wedi ei weinyddu ar %{domain} title: Ynghylch accounts: + errors: + cannot_be_added_to_collections: Does dim modd ychwanegu'r cyfrif hwn at gasgliadau. followers: few: Dilynwyr many: Dilynwyr @@ -1862,16 +1864,22 @@ cy: body: 'Caswoch eich crybwyll gan %{name} yn:' subject: Cawsoch eich crybwyll gan %{name} title: Crywbylliad newydd + moderation_warning: + subject: Rydych wedi derbyn rhybudd gan gymedrolwr poll: subject: Mae arolwg gan %{name} wedi dod i ben quote: body: 'Mae %{name} wedi dyfynnu eich postiad :' subject: Mae %{name} wedi dyfynnu eich postiad title: Dyfyniad newydd + quoted_update: + subject: Golygodd %{name} bostiad rydych chi wedi'i ddyfynnu reblog: body: 'Cafodd eich postiad ei hybu gan %{name}:' subject: Rhoddodd %{name} hwb i'ch postiad title: Hwb newydd + severed_relationships: + subject: Rydych chi wedi colli cysylltiadau oherwydd penderfyniad cymedroli status: subject: "%{name} newydd ei bostio" update: @@ -2355,3 +2363,6 @@ cy: not_supported: Nid yw'r porwr hwn yn cynnal allweddi diogelwch otp_required: I ddefnyddio allweddi diogelwch, galluogwch ddilysu dau ffactor yn gyntaf. registered_on: Cofrestrwyd ar %{date} + wrapstodon: + description: Gweler sut defnyddiodd %{name} Mastodon eleni! + title: Wrapstodon %{year} ar gyfer %{name} diff --git a/config/locales/da.yml b/config/locales/da.yml index 09bcb6fdf07fe9..ea5cbe33370ba6 100644 --- a/config/locales/da.yml +++ b/config/locales/da.yml @@ -7,6 +7,8 @@ da: hosted_on: Mastodon hostet på %{domain} title: Om accounts: + errors: + cannot_be_added_to_collections: Denne konto kan ikke tilføjes til samlinger. followers: one: Følger other: Følgere @@ -846,6 +848,7 @@ da: publish_statistics: Udgiv statistik title: Opdagelse trends: Trends + wrapstodon: Wrapstodon domain_blocks: all: Til alle disabled: Til ingen @@ -2186,4 +2189,5 @@ da: otp_required: For at bruge sikkerhedsnøgler skal tofaktorgodkendelse først aktiveres. registered_on: Registreret d. %{date} wrapstodon: + description: Se hvordan %{name} brugte Mastodon i år! title: Wrapstodon %{year} for %{name} diff --git a/config/locales/de.yml b/config/locales/de.yml index 13fd701bca60a1..0ecff0e276342d 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -7,6 +7,8 @@ de: hosted_on: Mastodon, gehostet auf %{domain} title: Über accounts: + errors: + cannot_be_added_to_collections: Dieses Konto darf keinen Sammlungen hinzugefügt werden. followers: one: Follower other: Follower @@ -54,15 +56,15 @@ de: title: Rolle für %{username} ändern confirm: Bestätigen confirmed: Bestätigt - confirming: Bestätigung + confirming: Bestätigung ausstehend custom: Angepasst delete: Daten löschen deleted: Gelöscht demote: Zurückstufen - destroyed_msg: Daten von %{username} wurden zum Löschen in die Warteschlange eingereiht + destroyed_msg: Die Daten von %{username} befinden sich nun in der Warteschlange und werden in Kürze gelöscht disable: Einfrieren disable_sign_in_token_auth: E-Mail-Token-Authentisierung deaktivieren - disable_two_factor_authentication: Zwei-Faktor-Authentisierung deaktivieren + disable_two_factor_authentication: Zwei-Faktor-Authentisierung (2FA) deaktivieren disabled: Eingefroren display_name: Anzeigename domain: Domain @@ -80,7 +82,7 @@ de: invite_request_text: Begründung für das Beitreten invited_by: Eingeladen von ip: IP-Adresse - joined: Mitglied seit + joined: Registriert am location: all: Alle local: Lokal @@ -103,12 +105,12 @@ de: most_recent_activity: Letzte Aktivität most_recent_ip: Letzte IP-Adresse no_account_selected: Keine Konten wurden geändert, da keine ausgewählt wurden - no_limits_imposed: Keine Beschränkungen + no_limits_imposed: Keine Einschränkungen no_role_assigned: Keine Rolle zugewiesen not_subscribed: Nicht abonniert pending: Überprüfung ausstehend perform_full_suspension: Sperren - previous_strikes: Vorherige Maßnahmen + previous_strikes: Frühere Maßnahmen previous_strikes_description_html: one: Gegen dieses Konto wurde eine Maßnahme verhängt. other: Gegen dieses Konto wurden %{count} Maßnahmen verhängt. @@ -129,14 +131,14 @@ de: resend_confirmation: already_confirmed: Dieses Profil wurde bereits bestätigt send: Bestätigungslink erneut zusenden - success: Bestätigungslink erfolgreich gesendet! + success: Bestätigungslink erfolgreich verschickt! reset: Zurücksetzen reset_password: Passwort zurücksetzen resubscribe: Erneut abonnieren role: Rolle search: Suchen search_same_email_domain: Andere Konten mit der gleichen E-Mail-Domain - search_same_ip: Andere Konten mit derselben IP-Adresse + search_same_ip: Andere Konten mit der gleichen IP-Adresse security: Sicherheit security_measures: only_password: Nur Passwort @@ -145,7 +147,7 @@ de: sensitized: Mit Inhaltswarnung versehen shared_inbox_url: Geteilte Posteingangsadresse show: - created_reports: Erstellte Meldungen + created_reports: Eingereichte Meldungen targeted_reports: Von Anderen gemeldet silence: Stummschalten silenced: Stummgeschaltet @@ -209,7 +211,7 @@ de: disable_custom_emoji: Eigenes Emoji deaktivieren disable_relay: Relais deaktivieren disable_sign_in_token_auth_user: E-Mail-Token-Authentisierung für dieses Konto deaktivieren - disable_user: Benutzer*in deaktivieren + disable_user: Profil deaktivieren enable_custom_emoji: Eigenes Emoji aktivieren enable_relay: Relais aktivieren enable_sign_in_token_auth_user: E-Mail-Token-Authentisierung für dieses Konto aktivieren @@ -333,8 +335,8 @@ de: scheduled_for: Geplant für %{time} scheduled_msg: Ankündigung ist zur Veröffentlichung vorgemerkt! title: Ankündigungen - unpublish: Veröffentlichung rückgängig machen - unpublished_msg: Ankündigung ist jetzt nicht mehr sichtbar! + unpublish: Ankündigung zurücknehmen + unpublished_msg: Ankündigung erfolgreich zurückgenommen! updated_msg: Ankündigung erfolgreich aktualisiert! critical_update_pending: Kritisches Update ausstehend custom_emojis: @@ -376,7 +378,7 @@ de: interactions: Interaktionen media_storage: Medien new_users: neue Profile - opened_reports: eingereichte Meldungen + opened_reports: Unbearbeitete Meldungen pending_appeals_html: one: "%{count} ausstehender Einspruch" other: "%{count} ausstehende Einsprüche" @@ -846,6 +848,7 @@ de: publish_statistics: Statistiken veröffentlichen title: Entdecken trends: Trends + wrapstodon: Wrapstodon domain_blocks: all: Allen disabled: Niemandem @@ -1505,7 +1508,7 @@ de: too_large: Datei ist zu groß failures: Fehler imported: Importiert - mismatched_types_warning: Möglicherweise hast du den falschen Typ für diesen Import ausgewählt. Bitte überprüfe alles noch einmal. + mismatched_types_warning: Möglicherweise hast du das falsche Format für diesen Import ausgewählt. Bitte überprüfe alles noch einmal. modes: merge: Zusammenführen merge_long: Bestehende Datensätze beibehalten und neue hinzufügen @@ -2025,7 +2028,7 @@ de: too_many_requests: Der Übersetzungsdienst hat kürzlich zu viele Anfragen erhalten. two_factor_authentication: add: Hinzufügen - disable: Zwei-Faktor-Authentisierung deaktivieren + disable: Zwei-Faktor-Authentisierung (2FA) deaktivieren disabled_success: Zwei-Faktor-Authentisierung erfolgreich deaktiviert edit: Bearbeiten enabled: Zwei-Faktor-Authentisierung (2FA) ist aktiviert @@ -2155,7 +2158,7 @@ de: users: follow_limit_reached: Du kannst nicht mehr als %{limit} Profilen folgen go_to_sso_account_settings: Kontoeinstellungen des Identitätsanbieters aufrufen - invalid_otp_token: Ungültiger Code der Zwei-Faktor-Authentisierung + invalid_otp_token: Ungültiger Code der Zwei-Faktor-Authentisierung (2FA) otp_lost_help_html: Wenn du sowohl die E-Mail-Adresse als auch das Passwort nicht mehr weißt, melde dich bitte bei uns unter %{email} rate_limited: Zu viele Authentisierungsversuche. Bitte versuche es später noch einmal. seamless_external_login: Du bist über einen externen Dienst angemeldet, daher sind Passwort- und E-Mail-Einstellungen nicht verfügbar. @@ -2186,4 +2189,5 @@ de: otp_required: Um Sicherheitsschlüssel zu verwenden, aktiviere zunächst die Zwei-Faktor-Authentisierung. registered_on: Registriert am %{date} wrapstodon: + description: Sieh dir an, wie %{name} dieses Jahr Mastodon verwendet hat! title: Wrapstodon %{year} für %{name} diff --git a/config/locales/devise.el.yml b/config/locales/devise.el.yml index eaab37f48d94b9..86134b9491f40b 100644 --- a/config/locales/devise.el.yml +++ b/config/locales/devise.el.yml @@ -8,7 +8,7 @@ el: failure: already_authenticated: Έχεις ήδη συνδεθεί. closed_registrations: Η προσπάθεια εγγραφής σας έχει αποκλειστεί λόγω μιας πολιτικής δικτύου. Αν πιστεύετε ότι πρόκειται για σφάλμα, επικοινωνήστε με το %{email}. - inactive: Ο λογαριασμός σου δεν έχει ενεργοποιηθεί ακόμα. + inactive: Ο λογαριασμός σου δεν έχει ενεργοποιηθεί ακόμη. invalid: Λάθος %{authentication_keys} ή συνθηματικό. last_attempt: Έχεις μια ακόμα προσπάθεια πριν κλειδωθεί ο λογαριασμός σου. locked: Ο λογαριασμός σου κλειδώθηκε. diff --git a/config/locales/devise.en-GB.yml b/config/locales/devise.en-GB.yml index 118423c966cfa4..c4650577d77717 100644 --- a/config/locales/devise.en-GB.yml +++ b/config/locales/devise.en-GB.yml @@ -23,66 +23,66 @@ en-GB: action: Verify email address action_with_app: Confirm and return to %{app} explanation: You have created an account on %{host} with this email address. You are one click away from activating it. If this wasn't you, please ignore this email. - explanation_when_pending: You applied for an invite to %{host} with this email address. Once you confirm your e-mail address, we will review your application. You can log in to change your details or delete your account, but you cannot access most of the functions until your account is approved. If your application is rejected, your data will be removed, so no further action will be required from you. If this wasn't you, please ignore this email. + explanation_when_pending: You applied for an invite to %{host} with this email address. Once you confirm your email address, we will review your application. You can log in to change your details or delete your account, but you cannot access most of the functions until your account is approved. If your application is rejected, your data will be removed, so no further action will be required from you. If this wasn't you, please ignore this email. extra_html: Please also check out the rules of the server and our terms of service. - subject: 'Mastodon: Confirmation instructions for %{instance}' + subject: 'Mastodon: confirmation instructions for %{instance}' title: Verify email address email_changed: explanation: 'The email address for your account is being changed to:' extra: If you did not change your email, it is likely that someone has gained access to your account. Please change your password immediately or contact the server admin if you're locked out of your account. - subject: 'Mastodon: Email changed' + subject: 'Mastodon: email changed' title: New email address password_change: explanation: The password for your account has been changed. extra: If you did not change your password, it is likely that someone has gained access to your account. Please change your password immediately or contact the server admin if you're locked out of your account. - subject: 'Mastodon: Password changed' + subject: 'Mastodon: password changed' title: Password changed reconfirmation_instructions: explanation: Confirm the new address to change your email. extra: If this change wasn't initiated by you, please ignore this email. The email address for the Mastodon account won't change until you access the link above. - subject: 'Mastodon: Confirm email for %{instance}' + subject: 'Mastodon: confirm email for %{instance}' title: Verify email address reset_password_instructions: action: Change password explanation: You requested a new password for your account. extra: If you didn't request this, please ignore this email. Your password won't change until you access the link above and create a new one. - subject: 'Mastodon: Reset password instructions' + subject: 'Mastodon: reset password instructions' title: Password reset two_factor_disabled: - explanation: Login is now possible using only e-mail address and password. - subject: 'Mastodon: Two-factor authentication disabled' + explanation: Login is now possible using only email address and password. + subject: 'Mastodon: two-factor authentication disabled' subtitle: Two-factor authentication for your account has been disabled. title: 2FA disabled two_factor_enabled: explanation: A token generated by the paired TOTP app will be required for login. - subject: 'Mastodon: Two-factor authentication enabled' + subject: 'Mastodon: two-factor authentication enabled' subtitle: Two-factor authentication has been enabled for your account. title: 2FA enabled two_factor_recovery_codes_changed: explanation: The previous recovery codes have been invalidated and new ones generated. - subject: 'Mastodon: Two-factor recovery codes re-generated' + subject: 'Mastodon: two-factor recovery codes re-generated' subtitle: The previous recovery codes have been invalidated and new ones generated. title: 2FA recovery codes changed unlock_instructions: - subject: 'Mastodon: Unlock instructions' + subject: 'Mastodon: unlock instructions' webauthn_credential: added: explanation: The following security key has been added to your account - subject: 'Mastodon: New security key' + subject: 'Mastodon: new security key' title: A new security key has been added deleted: explanation: The following security key has been deleted from your account - subject: 'Mastodon: Security key deleted' + subject: 'Mastodon: security key deleted' title: One of your security keys has been deleted webauthn_disabled: explanation: Authentication with security keys has been disabled for your account. extra: Login is now possible using only the token generated by the paired TOTP app. - subject: 'Mastodon: Authentication with security keys disabled' + subject: 'Mastodon: authentication with security keys disabled' title: Security keys disabled webauthn_enabled: explanation: Security key authentication has been enabled for your account. extra: Your security key can now be used for login. - subject: 'Mastodon: Security key authentication enabled' + subject: 'Mastodon: security key authentication enabled' title: Security keys enabled omniauth_callbacks: failure: Could not authenticate you from %{kind} because “%{reason}”. diff --git a/config/locales/doorkeeper.de.yml b/config/locales/doorkeeper.de.yml index 6d0e3010af7523..98a6d45da5f355 100644 --- a/config/locales/doorkeeper.de.yml +++ b/config/locales/doorkeeper.de.yml @@ -101,7 +101,7 @@ de: temporarily_unavailable: Der Autorisierungs-Server ist aufgrund von zwischenzeitlicher Überlastung oder Wartungsarbeiten derzeit nicht in der Lage, die Anfrage zu bearbeiten. unauthorized_client: Der Client ist nicht dazu autorisiert, diese Anfrage mit dieser Methode auszuführen. unsupported_grant_type: Der Autorisierungs-Typ wird nicht vom Autorisierungs-Server unterstützt. - unsupported_response_type: Der Autorisierungs-Server unterstützt diesen Antwort-Typ nicht. + unsupported_response_type: Der Autorisierungsserver unterstützt dieses Antwortformat nicht. flash: applications: create: diff --git a/config/locales/doorkeeper.en-GB.yml b/config/locales/doorkeeper.en-GB.yml index 63a4575e83ce51..5b4b99858cccdb 100644 --- a/config/locales/doorkeeper.en-GB.yml +++ b/config/locales/doorkeeper.en-GB.yml @@ -96,7 +96,7 @@ en-GB: expired: The access token expired revoked: The access token was revoked unknown: The access token is invalid - resource_owner_authenticator_not_configured: Resource Owner find failed due to Doorkeeper.configure.resource_owner_authenticator being unconfiged. + resource_owner_authenticator_not_configured: Resource Owner find failed due to Doorkeeper.configure.resource_owner_authenticator being unconfigured. server_error: The authorisation server encountered an unexpected condition which prevented it from fulfilling the request. temporarily_unavailable: The authorisation server is currently unable to handle the request due to a temporary overloading or maintenance of the server. unauthorized_client: The client is not authorised to perform this request using this method. @@ -130,7 +130,7 @@ en-GB: crypto: End-to-end encryption favourites: Favourites filters: Filters - follow: Follows, Mutes and Blocks + follow: Follows, Mutes, and Blocks follows: Follows lists: Lists media: Media attachments diff --git a/config/locales/el.yml b/config/locales/el.yml index a17c20bdece415..ddc822f1ba7e20 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -7,6 +7,8 @@ el: hosted_on: Το Mastodon φιλοξενείται στο %{domain} title: Σχετικά accounts: + errors: + cannot_be_added_to_collections: Αυτός ο λογαριασμός δεν μπορεί να προστεθεί σε συλλογές. followers: one: Ακόλουθος other: Ακόλουθοι @@ -308,7 +310,7 @@ el: update_status_html: Ο/Η %{name} ενημέρωσε την ανάρτηση του/της %{target} update_user_role_html: Ο/Η %{name} άλλαξε τον ρόλο %{target} update_username_block_html: "%{name} ενημέρωσε κανόνα για ονόματα χρηστών που περιέχουν %{target}" - deleted_account: διαγεγραμμένος λογαριασμός + deleted_account: λογαριασμός διαγράφηκε empty: Δεν βρέθηκαν αρχεία καταγραφής. filter_by_action: Φιλτράρισμα ανά ενέργεια filter_by_user: Φιλτράρισμα ανά χρήστη @@ -751,7 +753,7 @@ el: description_html: Με τους ρόλους χρηστών, μπορείς να προσαρμόσεις σε ποιες λειτουργίες και περιοχές του Mastodon, οι χρήστες σας μπορούν να έχουν πρόσβαση. edit: Επεξεργασία ρόλου '%{name}' everyone: Προεπιλεγμένα δικαιώματα - everyone_full_description_html: Αυτός είναι ο βασικός ρόλος που επηρεάζει όλους τους χρήστες, ακόμη και εκείνους που δεν έχουν κάποιον ρόλο. Όλοι οι άλλοι ρόλοι κληρονομούν δικαιώματα από αυτόν. + everyone_full_description_html: Αυτός είναι ο βασικός ρόλος που επηρεάζει όλους τους χρήστες, ακόμα και εκείνους που δεν έχουν κάποιον ρόλο. Όλοι οι άλλοι ρόλοι κληρονομούν δικαιώματα από αυτόν. permissions_count: one: "%{count} δικαίωμα" other: "%{count} δικαιώματα" @@ -805,7 +807,7 @@ el: delete: Διαγραφή description_html: Ενώ οι περισσότεροι ισχυρίζονται ότι έχουν διαβάσει και συμφωνούν με τους όρους της υπηρεσίας, συνήθως οι άνθρωποι δεν διαβάζουν μέχρι μετά την εμφάνιση ενός προβλήματος. Κάνε ευκολότερο να δουν τους κανόνες του διακομιστή σας με μια ματιά παρέχοντας τους σε μια λίστα. Προσπάθησε να κρατήσεις τους μεμονωμένους κανόνες σύντομους και απλούς, αλλά προσπάθησε να μην τους χωρίσεις σε πολλά ξεχωριστά αντικείμενα. edit: Επεξεργασία κανόνα - empty: Δεν έχουν οριστεί ακόμα κανόνες διακομιστή. + empty: Δεν έχουν οριστεί ακόμη κανόνες διακομιστή. move_down: Μετακίνηση κάτω move_up: Μετακίνηση πάνω title: Κανόνες διακομιστή @@ -846,6 +848,7 @@ el: publish_statistics: Δημοσίευση στατιστικών title: Ανακάλυψη trends: Τάσεις + wrapstodon: Wrapstodon domain_blocks: all: Για όλους disabled: Για κανέναν @@ -1015,7 +1018,7 @@ el: going_live_on_html: Ενεργό, σε ισχύ από %{date} history: Ιστορικό live: Ενεργό - no_history: Δεν υπάρχουν ακόμα καταγεγραμμένες αλλαγές στους όρους παροχής υπηρεσιών. + no_history: Δεν υπάρχουν ακόμη καταγεγραμμένες αλλαγές στους όρους παροχής υπηρεσιών. no_terms_of_service_html: Δεν έχετε ρυθμίσει τους όρους της υπηρεσίας. Οι όροι της υπηρεσίας αποσκοπούν στην παροχή σαφήνειας και την προστασία σου από πιθανές υποχρεώσεις σε διαφορές με τους χρήστες σου. notified_on_html: Οι χρήστες ειδοποιήθηκαν στις %{date} notify_users: Ειδοποίηση χρηστών @@ -1137,7 +1140,7 @@ el: disable: Απενεργοποίηση disabled: Απενεργοποιημένα edit: Επεξεργασία σημείου τερματισμού - empty: Δεν έχεις ακόμα ρυθμισμένα σημεία τερματισμού webhook. + empty: Δεν έχεις ακόμη ρυθμισμένα σημεία τερματισμού webhook. enable: Ενεργοποίηση enabled: Ενεργό enabled_events: @@ -1226,7 +1229,7 @@ el: apply_for_account: Ζήτα έναν λογαριασμό captcha_confirmation: help_html: Εάν αντιμετωπίζεις προβλήματα με την επίλυση του CAPTCHA, μπορείς να επικοινωνήσεις μαζί μας μέσω %{email} και μπορούμε να σε βοηθήσουμε. - hint_html: Και κάτι ακόμα! Πρέπει να επιβεβαιώσουμε ότι είσαι άνθρωπος (αυτό γίνεται για να κρατήσουμε μακριά το σπαμ!). Λύσε το CAPTCHA παρακάτω και κάνε κλικ "Συνέχεια". + hint_html: Και κάτι ακόμα! Πρέπει να επιβεβαιώσουμε ότι είσαι άνθρωπος (αυτό γίνεται για να κρατήσουμε μακριά το σπαμ!). Λύσε το CAPTCHA παρακάτω και πάτα "Συνέχεια". title: Ελεγχος ασφαλείας confirmations: awaiting_review: Η διεύθυνση email σου επιβεβαιώθηκε! Το προσωπικό του %{domain} εξετάζει τώρα την εγγραφή σου. Θα λάβεις ένα email εάν εγκρίνουν τον λογαριασμό σου! @@ -1827,7 +1830,7 @@ el: over_total_limit: Έχεις υπερβεί το όριο των %{limit} προγραμματισμένων αναρτήσεων too_soon: η ημερομηνία πρέπει να είναι στο μέλλον self_destruct: - lead_html: Δυστυχώς, το %{domain} κλείνει οριστικά. Αν είχατε λογαριασμό εκεί, δεν θα μπορείτε να συνεχίσετε τη χρήση του, αλλά μπορείτε ακόμα να ζητήσετε ένα αντίγραφο ασφαλείας των δεδομένων σας. + lead_html: Δυστυχώς, το %{domain} κλείνει οριστικά. Αν είχατε λογαριασμό εκεί, δεν θα μπορείτε να συνεχίσετε τη χρήση του, αλλά μπορείτε ακόμη να ζητήσετε ένα αντίγραφο ασφαλείας των δεδομένων σας. title: Αυτός ο διακομιστής κλείνει οριστικά sessions: activity: Τελευταία δραστηριότητα @@ -1967,7 +1970,7 @@ el: ignore_favs: Αγνόηση αγαπημένων ignore_reblogs: Αγνόηση ενισχύσεων interaction_exceptions: Εξαιρέσεις βασισμένες σε αλληλεπιδράσεις - interaction_exceptions_explanation: Σημείωσε ότι δεν υπάρχει εγγύηση για πιθανή διαγραφή αναρτήσεων αν αυτά πέσουν κάτω από το όριο αγαπημένων ή ενισχύσεων ακόμα και αν κάποτε το είχαν ξεπεράσει. + interaction_exceptions_explanation: Σημείωσε ότι δεν υπάρχει εγγύηση για πιθανή διαγραφή αναρτήσεων αν αυτά πέσουν κάτω από το όριο αγαπημένων ή ενισχύσεων ακόμη και αν κάποτε το είχαν ξεπεράσει. keep_direct: Διατήρηση άμεσων μηνυμάτων keep_direct_hint: Δεν διαγράφει κανένα από τα άμεσα μηνύματά σου keep_media: Διατήρηση αναρτήσεων με συνημμένα πολυμέσων @@ -2092,8 +2095,8 @@ el: disable: Δεν μπορείς πλέον να χρησιμοποιήσεις τον λογαριασμό σου, αλλά το προφίλ σου και άλλα δεδομένα παραμένουν άθικτα. Μπορείς να ζητήσεις ένα αντίγραφο ασφαλείας των δεδομένων σου, να αλλάξεις τις ρυθμίσεις του λογαριασμού σου ή να διαγράψεις τον λογαριασμό σου. mark_statuses_as_sensitive: Μερικές από τις αναρτήσεις σου έχουν επισημανθεί ως ευαίσθητες από τους συντονιστές του %{instance}. Αυτό σημαίνει ότι οι άνθρωποι θα πρέπει να πατήσουν τα πολυμέσα στις αναρτήσεις πριν εμφανιστεί μια προεπισκόπηση. Μπορείς να επισημάνεις τα πολυμέσα ως ευαίσθητα όταν δημοσιεύεις στο μέλλον. sensitive: Από δω και στο εξής, όλα τα μεταφορτωμένα αρχεία πολυμέσων σου θα επισημανθούν ως ευαίσθητα και κρυμμένα πίσω από μια προειδοποίηση που πρέπει να πατηθεί. - silence: Μπορείς ακόμα να χρησιμοποιείς τον λογαριασμό σου, αλλά μόνο άτομα που σε ακολουθούν ήδη θα δουν τις αναρτήσεις σου σε αυτόν τον διακομιστή και μπορεί να αποκλειστείς από διάφορες δυνατότητες ανακάλυψης. Ωστόσο, οι άλλοι μπορούν ακόμα να σε ακολουθήσουν με μη αυτόματο τρόπο. - suspend: Δε μπορείς πλέον να χρησιμοποιήσεις τον λογαριασμό σου και το προφίλ σου και άλλα δεδομένα δεν είναι πλέον προσβάσιμα. Μπορείς ακόμα να συνδεθείς για να αιτηθείς αντίγραφο των δεδομένων σου μέχρι να αφαιρεθούν πλήρως σε περίπου 30 μέρες αλλά, θα διατηρήσουμε κάποια βασικά δεδομένα για να σε αποτρέψουμε να παρακάμψεις την αναστολή. + silence: Μπορείς ακόμη να χρησιμοποιείς τον λογαριασμό σου, αλλά μόνο άτομα που σε ακολουθούν ήδη θα δουν τις αναρτήσεις σου σε αυτόν τον διακομιστή και μπορεί να αποκλειστείς από διάφορες δυνατότητες ανακάλυψης. Ωστόσο, οι άλλοι μπορούν ακόμη να σε ακολουθήσουν με μη αυτόματο τρόπο. + suspend: Δε μπορείς πλέον να χρησιμοποιήσεις τον λογαριασμό σου, και το προφίλ σου και άλλα δεδομένα δεν είναι πλέον προσβάσιμα. Μπορείς ακόμη να συνδεθείς για να αιτηθείς αντίγραφο των δεδομένων σου μέχρι να αφαιρεθούν πλήρως σε περίπου 30 μέρες αλλά, θα διατηρήσουμε κάποια βασικά δεδομένα για να σε αποτρέψουμε να παρακάμψεις την αναστολή. reason: 'Αιτιολογία:' statuses: 'Αναφερόμενες αναρτήσεις:' subject: @@ -2161,7 +2164,7 @@ el: seamless_external_login: Επειδή έχεις συνδεθεί μέσω τρίτης υπηρεσίας, οι ρυθμίσεις συνθηματικού και email δεν είναι διαθέσιμες. signed_in_as: 'Έχεις συνδεθεί ως:' verification: - extra_instructions_html: Συμβουλή: Ο σύνδεσμος στην ιστοσελίδα σου μπορεί να είναι αόρατος. Το σημαντικό μέρος είναι το rel="me" που αποτρέπει την μίμηση σε ιστοσελίδες με περιεχόμενο παραγόμενο από χρήστες. Μπορείς ακόμη να χρησιμοποιήσεις μια ετικέτα συνδέσμου στην κεφαλίδα της σελίδας αντί για a, αλλά ο κώδικας HTML πρέπει να είναι προσβάσιμος χωρίς την εκτέλεση JavaScript. + extra_instructions_html: Συμβουλή: Ο σύνδεσμος στην ιστοσελίδα σου μπορεί να είναι αόρατος. Το σημαντικό μέρος είναι το rel="me" που αποτρέπει την μίμηση σε ιστοσελίδες με περιεχόμενο παραγόμενο από χρήστες. Μπορείς ακόμα να χρησιμοποιήσεις μια ετικέτα συνδέσμου στην κεφαλίδα της σελίδας αντί για a, αλλά ο κώδικας HTML πρέπει να είναι προσβάσιμος χωρίς την εκτέλεση JavaScript. here_is_how: Δείτε πώς hint_html: Η επαλήθευση της ταυτότητας στο Mastodon είναι για όλους. Βασισμένο σε ανοιχτά πρότυπα ιστού, τώρα και για πάντα δωρεάν. Το μόνο που χρειάζεσαι είναι μια προσωπική ιστοσελίδα που ο κόσμος να σε αναγνωρίζει από αυτή. Όταν συνδέεσαι σε αυτήν την ιστοσελίδα από το προφίλ σου, θα ελέγξουμε ότι η ιστοσελίδα συνδέεται πίσω στο προφίλ σου και θα δείξει μια οπτική ένδειξη σε αυτό. instructions_html: Αντέγραψε και επικόλλησε τον παρακάτω κώδικα στην HTML της ιστοσελίδας σου. Στη συνέχεια, πρόσθεσε τη διεύθυνση της ιστοσελίδας σου σε ένα από τα επιπλέον πεδία στο προφίλ σου από την καρτέλα "Επεξεργασία προφίλ" και αποθήκευσε τις αλλαγές. @@ -2181,9 +2184,10 @@ el: success: Το κλειδί ασφαλείας σου διαγράφηκε με επιτυχία. invalid_credential: Άκυρο κλειδί ασφαλείας nickname_hint: Βάλε το ψευδώνυμο του νέου κλειδιού ασφαλείας σου - not_enabled: Δεν έχεις ενεργοποιήσει το WebAuthn ακόμα + not_enabled: Δεν έχεις ενεργοποιήσει το WebAuthn ακόμη not_supported: Αυτό το πρόγραμμα περιήγησης δεν υποστηρίζει κλειδιά ασφαλείας otp_required: Για να χρησιμοποιήσεις κλειδιά ασφαλείας, ενεργοποίησε πρώτα την ταυτοποίηση δύο παραγόντων. registered_on: Εγγραφή στις %{date} wrapstodon: + description: Δείτε πώς ο/η %{name} χρησιμοποίησε το Mastodon φέτος! title: Wrapstodon %{year} για %{name} diff --git a/config/locales/en-GB.yml b/config/locales/en-GB.yml index 7a08b1c63e853d..e616adfedc13a6 100644 --- a/config/locales/en-GB.yml +++ b/config/locales/en-GB.yml @@ -7,6 +7,8 @@ en-GB: hosted_on: Mastodon hosted on %{domain} title: About accounts: + errors: + cannot_be_added_to_collections: This account cannot be added to collections. followers: one: Follower other: Followers @@ -102,7 +104,7 @@ en-GB: moderation_notes: Moderation notes most_recent_activity: Most recent activity most_recent_ip: Most recent IP - no_account_selected: No accounts were changed as none were selected + no_account_selected: No accounts were changed, as none were selected no_limits_imposed: No limits imposed no_role_assigned: No role assigned not_subscribed: Not subscribed @@ -154,7 +156,7 @@ en-GB: subscribe: Subscribe suspend: Suspend suspended: Suspended - suspension_irreversible: The data of this account has been irreversibly deleted. You can unsuspend the account to make it usable but it will not recover any data it previously had. + suspension_irreversible: The data of this account has been irreversibly deleted. You can unsuspend the account to make it usable, but it will not recover any data it previously had. suspension_reversible_hint_html: The account has been suspended, and the data will be fully removed on %{date}. Until then, the account can be restored without any ill effects. If you wish to remove all of the account's data immediately, you can do so below. title: Accounts unblock_email: Unblock email address @@ -273,7 +275,7 @@ en-GB: destroy_unavailable_domain_html: "%{name} stopped delivery to domain %{target}" destroy_user_role_html: "%{name} deleted %{target} role" destroy_username_block_html: "%{name} removed rule for usernames containing %{target}" - disable_2fa_user_html: "%{name} disabled two factor requirement for user %{target}" + disable_2fa_user_html: "%{name} disabled two-factor requirement for user %{target}" disable_custom_emoji_html: "%{name} disabled emoji %{target}" disable_relay_html: "%{name} disabled the relay %{target}" disable_sign_in_token_auth_user_html: "%{name} disabled email token authentication for %{target}" @@ -326,7 +328,7 @@ en-GB: title: New announcement preview: disclaimer: As users cannot opt out of them, email notifications should be limited to important announcements such as personal data breach or server closure notifications. - explanation_html: 'The email will be sent to %{display_count} users. The following text will be included in the e-mail:' + explanation_html: 'The email will be sent to %{display_count} users. The following text will be included in the email:' title: Preview announcement notification publish: Publish published_msg: Announcement successfully published! @@ -363,7 +365,7 @@ en-GB: not_permitted: You are not permitted to perform this action overwrite: Overwrite shortcode: Shortcode - shortcode_hint: At least 2 characters, only alphanumeric characters and underscores + shortcode_hint: At least two characters, only alphanumeric characters and underscores title: Custom emojis uncategorized: Uncategorised unlist: Unlist @@ -443,7 +445,7 @@ en-GB: private_comment: Private comment private_comment_hint: Comment about this domain limitation for internal use by the moderators. public_comment: Public comment - public_comment_hint: Comment about this domain limitation for the general public, if advertising the list of domain limitations is enabled. + public_comment_hint: Comment about this domain limitation for the general public if advertising the list of domain limitations is enabled. reject_media: Reject media files reject_media_hint: Removes locally stored media files and refuses to download any in the future. Irrelevant for suspensions reject_reports: Reject reports @@ -547,7 +549,7 @@ en-GB: content_policies: comment: Internal note description_html: You can define content policies that will be applied to all accounts from this domain and any of its subdomains. - limited_federation_mode_description_html: You can chose whether to allow federation with this domain. + limited_federation_mode_description_html: You can choose whether to allow federation with this domain. policies: reject_media: Reject media reject_reports: Reject reports @@ -616,15 +618,15 @@ en-GB: created_msg: Successfully added new IP rule delete: Delete expires_in: - '1209600': 2 weeks - '15778476': 6 months - '2629746': 1 month - '31556952': 1 year - '86400': 1 day - '94670856': 3 years + '1209600': two weeks + '15778476': six months + '2629746': one month + '31556952': one year + '86400': one day + '94670856': three years new: title: Create new IP rule - no_ip_block_selected: No IP rules were changed as none were selected + no_ip_block_selected: No IP rules were changed, as none were selected title: IP rules relationships: title: "%{acct}'s relationships" @@ -640,7 +642,7 @@ en-GB: inbox_url: Relay URL pending: Waiting for relay's approval save_and_enable: Save and enable - setup: Setup a relay connection + setup: Set up a relay connection signatures_not_enabled: Relays may not work correctly while secure mode or limited federation mode is enabled status: Status title: Relays @@ -657,12 +659,12 @@ en-GB: actions: delete_description_html: The reported posts will be deleted and a strike will be recorded to help you escalate on future infractions by the same account. mark_as_sensitive_description_html: The media in the reported posts will be marked as sensitive and a strike will be recorded to help you escalate on future infractions by the same account. - other_description_html: See more options for controlling the account's behaviour and customise communication to the reported account. + other_description_html: See more options for controlling the account's behaviour and customising communication to the reported account. resolve_description_html: No action will be taken against the reported account, no strike recorded, and the report will be closed. - silence_description_html: The account will be visible only to those who already follow it or manually look it up, severely limiting its reach. Can always be reverted. Closes all reports against this account. - suspend_description_html: The account and all its contents will be inaccessible and eventually deleted, and interacting with it will be impossible. Reversible within 30 days. Closes all reports against this account. + silence_description_html: The account will be visible only to those who already follow it or manually look it up, severely limiting its reach. This can always be reverted. This closes all reports against this account. + suspend_description_html: The account and all its contents will be inaccessible and eventually deleted, and interacting with it will be impossible. This is reversible within 30 days. This closes all reports against this account. actions_description_html: Decide which action to take to resolve this report. If you take a punitive action against the reported account, an email notification will be sent to them, except when the Spam category is selected. - actions_description_remote_html: Decide which action to take to resolve this report. This will only affect how your server communicates with this remote account and handle its content. + actions_description_remote_html: Decide which action to take to resolve this report. This will only affect how your server communicates with this remote account and handles its content. actions_no_posts: This report doesn't have any associated posts to delete add_to_report: Add more to report already_suspended_badges: @@ -724,7 +726,7 @@ en-GB: suspend_html: Suspend @%{acct}, making their profile and contents inaccessible and impossible to interact with close_report: 'Mark report #%{id} as resolved' close_reports_html: Mark all reports against @%{acct} as resolved - delete_data_html: Delete @%{acct}'s profile and contents 30 days from now unless they get unsuspended in the meantime + delete_data_html: Delete @%{acct}'s profile and contents 30 days from now, unless they get unsuspended in the meantime preview_preamble_html: "@%{acct} will receive a warning with the following contents:" record_strike_html: Record a strike against @%{acct} to help you escalate on future violations from this account send_email_html: Send @%{acct} a warning email @@ -748,7 +750,7 @@ en-GB: moderation: Moderation special: Special delete: Delete - description_html: With user roles, you can customize which functions and areas of Mastodon your users can access. + description_html: With user roles, you can customise which functions and areas of Mastodon your users can access. edit: Edit '%{name}' role everyone: Default permissions everyone_full_description_html: This is the base role affecting all users, even those without an assigned role. All other roles inherit permissions from it. @@ -815,11 +817,11 @@ en-GB: settings: about: manage_rules: Manage server rules - preamble: Provide in-depth information about how the server is operated, moderated, funded. - rules_hint: There is a dedicated area for rules that your users are expected to adhere to. + preamble: Provide in-depth information about how the server is operated, moderated, and funded. + rules_hint: There is a dedicated area for rules to which your users are expected to adhere. title: About allow_referrer_origin: - desc: When your users click links to external sites, their browser may send the address of your Mastodon server as the referrer. Disable this if this would uniquely identify your users, e.g. if this is a personal Mastodon server. + desc: When your users click links to external sites, their browser may send the address of your Mastodon server as the referrer. Disable this if this would uniquely identify your users, eg if this is a personal Mastodon server. title: Allow external sites to see your Mastodon server as a traffic source appearance: preamble: Customise Mastodon's web interface. @@ -846,6 +848,7 @@ en-GB: publish_statistics: Publish statistics title: Discovery trends: Trends + wrapstodon: Wrapstodon domain_blocks: all: To everyone disabled: To no one @@ -880,7 +883,7 @@ en-GB: delete: Delete uploaded file destroyed_msg: Site upload successfully deleted! software_updates: - critical_update: Critical — please update quickly + critical_update: Critical – please update quickly description: It is recommended to keep your Mastodon installation up to date to benefit from the latest fixes and features. Moreover, it is sometimes critical to update Mastodon in a timely manner to avoid security issues. For these reasons, Mastodon checks for updates every 30 minutes, and will notify you according to your email notification preferences. documentation_link: Learn more release_notes: Release notes @@ -889,7 +892,7 @@ en-GB: types: major: Major release minor: Minor release - patch: Patch release — bugfixes and easy to apply changes + patch: Patch release – bug fixes and easy to apply changes version: Version statuses: account: Author @@ -918,7 +921,7 @@ en-GB: replied_to_html: Replied to %{acct_link} status_changed: Post changed status_title: Post by @%{name} - title: Account posts - @%{name} + title: Account posts – @%{name} trending: Trending view_publicly: View publicly view_quoted_post: View quoted post @@ -973,7 +976,7 @@ en-GB: message_html: A critical Mastodon update is available, please update as quickly as possible. software_version_patch_check: action: See available updates - message_html: A bugfix Mastodon update is available. + message_html: A bug fix Mastodon update is available. upload_check_privacy_error: action: Check here for more information message_html: "Your web server is misconfigured. The privacy of your users is at risk." @@ -1020,7 +1023,7 @@ en-GB: notified_on_html: Users notified on %{date} notify_users: Notify users preview: - explanation_html: 'The email will be sent to %{display_count} users who have signed up before %{date}. The following text will be included in the e-mail:' + explanation_html: 'The email will be sent to %{display_count} users who have signed up before %{date}. The following text will be included in the email:' send_preview: Send preview to %{email} send_to_all: one: Send %{display_count} email @@ -1044,12 +1047,12 @@ en-GB: confirm_allow_provider: Are you sure you want to allow selected providers? confirm_disallow: Are you sure you want to disallow selected links? confirm_disallow_provider: Are you sure you want to disallow selected providers? - description_html: These are links that are currently being shared a lot by accounts that your server sees posts from. It can help your users find out what's going on in the world. No links are displayed publicly until you approve the publisher. You can also allow or reject individual links. + description_html: These are links that are currently being shared a lot by accounts from which your server sees posts. It can help your users find out what's going on in the world. No links are displayed publicly until you approve the publisher. You can also allow or reject individual links. disallow: Disallow link disallow_provider: Disallow publisher - no_link_selected: No links were changed as none were selected + no_link_selected: No links were changed, as none were selected publishers: - no_publisher_selected: No publishers were changed as none were selected + no_publisher_selected: No publishers were changed, as none were selected shared_by_over_week: one: Shared by one person over the last week other: Shared by %{count} people over the last week @@ -1074,7 +1077,7 @@ en-GB: description_html: These are posts that your server knows about that are currently being shared and favourited a lot at the moment. It can help your new and returning users to find more people to follow. No posts are displayed publicly until you approve the author, and the author allows their account to be suggested to others. You can also allow or reject individual posts. disallow: Disallow post disallow_account: Disallow author - no_status_selected: No trending posts were changed as none were selected + no_status_selected: No trending posts were changed, as none were selected not_discoverable: Author has not opted-in to being discoverable shared_by: one: Shared or favourited one time @@ -1090,7 +1093,7 @@ en-GB: tag_uses_measure: total uses description_html: These are hashtags that are currently appearing in a lot of posts that your server sees. It can help your users find out what people are talking the most about at the moment. No hashtags are displayed publicly until you approve them. listable: Can be suggested - no_tag_selected: No tags were changed as none were selected + no_tag_selected: No tags were changed, as none were selected not_listable: Won't be suggested not_trendable: Won't appear under trends not_usable: Cannot be used @@ -1243,7 +1246,7 @@ en-GB: description: prefix_invited_by_user: "@%{name} invites you to join this server of Mastodon!" prefix_sign_up: Sign up on Mastodon today! - suffix: With an account, you will be able to follow people, post updates and exchange messages with users from any Mastodon server and more! + suffix: With an account, you will be able to follow people, post updates, and exchange messages with users from any Mastodon server and more! didnt_get_confirmation: Didn't receive a confirmation link? dont_have_your_security_key: Don't have your security key? forgot_password: Forgot your password? @@ -1313,7 +1316,7 @@ en-GB: title: Author attribution challenge: confirm: Continue - hint_html: "Tip: We won't ask you for your password again for the next hour." + hint_html: "Tip: we won't ask you for your password again for the next hour." invalid_password: Invalid password prompt: Confirm password to continue crypto: @@ -1414,7 +1417,7 @@ en-GB: archive_takeout: date: Date download: Download your archive - hint_html: You can request an archive of your posts and uploaded media. The exported data will be in the ActivityPub format, readable by any compliant software. You can request an archive every 7 days. + hint_html: You can request an archive of your posts and uploaded media. The exported data will be in the ActivityPub format, readable by any compliant software. You can request an archive every seven days. in_progress: Compiling your archive... request: Request your archive size: Size @@ -1429,7 +1432,7 @@ en-GB: add_new: Add new errors: limit: You have already featured the maximum number of hashtags - hint_html: "What are featured hashtags? They are displayed prominently on your public profile and allow people to browse your public posts specifically under those hashtags. They are a great tool for keeping track of creative works or long-term projects." + hint_html: "Feature your most important hashtags on your profile. A great tool for keeping track of your creative works and long-term projects, featured hashtags are displayed prominently on your profile and allow quick access to your own posts." filters: contexts: account: Profiles @@ -1568,8 +1571,8 @@ en-GB: muting: Importing muted accounts type: Import type type_groups: - constructive: Follows & Bookmarks - destructive: Blocks & mutes + constructive: Follows and bookmarks + destructive: Blocks and mutes types: blocking: Blocking list bookmarks: Bookmarks @@ -1642,7 +1645,7 @@ en-GB: images_and_video: Cannot attach a video to a post that already contains images not_found: Media %{ids} not found or already attached to another post not_ready: Cannot attach files that have not finished processing. Try again in a moment! - too_many: Cannot attach more than 4 files + too_many: Cannot attach more than four files migrations: acct: Moved to cancel: Cancel redirect @@ -1776,11 +1779,11 @@ en-GB: privacy: hint_html: "Customise how you want your profile and your posts to be found. A variety of features in Mastodon can help you reach a wider audience when enabled. Take a moment to review these settings to make sure they fit your use case." privacy: Privacy - privacy_hint_html: Control how much you want to disclose for the benefit of others. People discover interesting profiles and cool apps by browsing other people's follows and seeing which apps they post from, but you may prefer to keep it hidden. + privacy_hint_html: Control how much you want to disclose for the benefit of others. People discover interesting profiles and cool apps by browsing other people's follows and seeing from which apps they post, but you may prefer to keep it hidden. reach: Reach reach_hint_html: Control whether you want to be discovered and followed by new people. Do you want your posts to appear on the Explore screen? Do you want other people to see you in their follow recommendations? Do you want to accept all new followers automatically, or have granular control over each one? search: Search - search_hint_html: Control how you want to be found. Do you want people to find you by what you've publicly posted about? Do you want people outside Mastodon to find your profile when searching the web? Please mind that total exclusion from all search engines cannot be guaranteed for public information. + search_hint_html: Control how you want to be found. Do you want people to find you by what you've publicly posted about? Do you want people outside Mastodon to find your profile when searching the web? Please bear in mind that total exclusion from all search engines cannot be guaranteed for public information. title: Privacy and reach privacy_policy: title: Privacy Policy @@ -1991,9 +1994,9 @@ en-GB: '7889238': 3 months min_age_label: Age threshold min_favs: Keep posts favourited at least - min_favs_hint: Doesn't delete any of your posts that has received at least this number of favourites. Leave blank to delete posts regardless of their number of favourites + min_favs_hint: Doesn't delete any of your posts that have received at least this number of favourites. Leave blank to delete posts regardless of their number of favourites min_reblogs: Keep posts boosted at least - min_reblogs_hint: Doesn't delete any of your posts that has been boosted at least this number of times. Leave blank to delete posts regardless of their number of boosts + min_reblogs_hint: Doesn't delete any of your posts that have been boosted at least this number of times. Leave blank to delete posts regardless of their number of boosts stream_entries: sensitive_content: Sensitive content strikes: @@ -2075,8 +2078,8 @@ en-GB: terms_of_service_changed: agreement: By continuing to use %{domain}, you are agreeing to these terms. If you disagree with the updated terms, you may terminate your agreement with %{domain} at any time by deleting your account. changelog: 'At a glance, here is what this update means for you:' - description: 'You are receiving this e-mail because we''re making some changes to our terms of service at %{domain}. These updates will take effect on %{date}. We encourage you to review the updated terms in full here:' - description_html: You are receiving this e-mail because we're making some changes to our terms of service at %{domain}. These updates will take effect on %{date}. We encourage you to review the updated terms in full here. + description: 'You are receiving this email because we''re making some changes to our terms of service at %{domain}. These updates will take effect on %{date}. We encourage you to review the updated terms in full here:' + description_html: You are receiving this email because we're making some changes to our terms of service at %{domain}. These updates will take effect on %{date}. We encourage you to review the updated terms in full here. sign_off: The %{domain} team subject: Updates to our terms of service subtitle: The terms of service of %{domain} are changing @@ -2139,9 +2142,9 @@ en-GB: follows_title: Who to follow follows_view_more: View more people to follow hashtags_recent_count: - one: "%{people} person in the past 2 days" - other: "%{people} people in the past 2 days" - hashtags_subtitle: Explore what’s trending since past 2 days + one: "%{people} person in the past two days" + other: "%{people} people in the past two days" + hashtags_subtitle: Explore what’s trending since the past two days hashtags_title: Trending hashtags hashtags_view_more: View more trending hashtags post_action: Compose @@ -2161,9 +2164,9 @@ en-GB: seamless_external_login: You are logged in via an external service, so password and email settings are not available. signed_in_as: 'Logged in as:' verification: - extra_instructions_html: Tip: The link on your website can be invisible. The important part is rel="me" which prevents impersonation on websites with user-generated content. You can even use a link tag in the header of the page instead of a, but the HTML must be accessible without executing JavaScript. + extra_instructions_html: Tip: the link on your website can be invisible. The important part is rel="me" which prevents impersonation on websites with user-generated content. You can even use a link tag in the header of the page instead of a, but the HTML must be accessible without executing JavaScript. here_is_how: Here's how - hint_html: "Verifying your identity on Mastodon is for everyone. Based on open web standards, now and forever free. All you need is a personal website that people recognize you by. When you link to this website from your profile, we will check that the website links back to your profile and show a visual indicator on it." + hint_html: "Verifying your identity on Mastodon is for everyone. Based on open web standards, now and forever free. All you need is a personal website that people recognise you by. When you link to this website from your profile, we will check that the website links back to your profile and show a visual indicator on it." instructions_html: Copy and paste the code below into the HTML of your website. Then add the address of your website into one of the extra fields on your profile from the "Edit profile" tab and save changes. verification: Verification verified_links: Your verified links @@ -2186,4 +2189,5 @@ en-GB: otp_required: To use security keys please enable two-factor authentication first. registered_on: Registered on %{date} wrapstodon: + description: See how %{name} used Mastodon this year! title: Wrapstodon %{year} for %{name} diff --git a/config/locales/en.yml b/config/locales/en.yml index 5625fae2f89312..441e61eb98ffa2 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -7,6 +7,8 @@ en: hosted_on: Mastodon hosted on %{domain} title: About accounts: + errors: + cannot_be_added_to_collections: This account cannot be added to collections. followers: one: Follower other: Followers @@ -1095,6 +1097,7 @@ en: title: Discovery trends: Trends visibilities: Visibilities + wrapstodon: Wrapstodon domain_blocks: all: To everyone disabled: To no one @@ -1617,6 +1620,13 @@ en: hint_html: "Tip: We won't ask you for your password again for the next hour." invalid_password: Invalid password prompt: Confirm password to continue + color_scheme: + auto: Auto + dark: Dark + light: Light + contrast: + auto: Auto + high: High crypto: errors: invalid_key: is not a valid Ed25519 or Curve25519 key @@ -2561,4 +2571,5 @@ en: otp_required: To use security keys please enable two-factor authentication first. registered_on: Registered on %{date} wrapstodon: + description: See how %{name} used Mastodon this year! title: Wrapstodon %{year} for %{name} diff --git a/config/locales/es-AR.yml b/config/locales/es-AR.yml index 097329cc5b8c4b..573c3ac859241e 100644 --- a/config/locales/es-AR.yml +++ b/config/locales/es-AR.yml @@ -7,6 +7,8 @@ es-AR: hosted_on: Mastodon alojado en %{domain} title: Información accounts: + errors: + cannot_be_added_to_collections: Esta cuenta no puede ser agregada a colecciones. followers: one: Seguidor other: Seguidores @@ -846,6 +848,7 @@ es-AR: publish_statistics: Publicar estadísticas title: Descubrí trends: Tendencias + wrapstodon: MastodonAnual domain_blocks: all: A todos disabled: A nadie @@ -2186,4 +2189,5 @@ es-AR: otp_required: Para usar llaves de seguridad, por favor, primero habilitá la autenticación de dos factores. registered_on: Registrado el %{date} wrapstodon: + description: "¡Enterate cómo %{name} usó Mastodon este año!" title: MastodonAnual %{year} para %{name} diff --git a/config/locales/es-MX.yml b/config/locales/es-MX.yml index 0ca9c86c048d3a..5fcefbce28d5e2 100644 --- a/config/locales/es-MX.yml +++ b/config/locales/es-MX.yml @@ -7,6 +7,8 @@ es-MX: hosted_on: Mastodon alojado en %{domain} title: Acerca de accounts: + errors: + cannot_be_added_to_collections: Esta cuenta no se puede añadir a las colecciones. followers: one: Seguidor other: Seguidores @@ -40,14 +42,14 @@ es-MX: avatar: Foto de perfil by_domain: Dominio change_email: - changed_msg: Correo cambiado exitosamente! + changed_msg: "¡Correo electrónico cambiado correctamente!" current_email: Correo electrónico actual label: Cambiar el correo electrónico new_email: Nuevo correo electrónico submit: Cambiar el correo electrónico title: Cambiar el correo electrónico de %{username} change_role: - changed_msg: Rol cambiado exitosamente! + changed_msg: "¡Rol cambiado correctamente!" edit_roles: Administrar roles de usuario label: Cambiar de rol no_role: Sin rol @@ -105,7 +107,7 @@ es-MX: no_account_selected: Ninguna cuenta se cambió como ninguna fue seleccionada no_limits_imposed: Sin límites impuestos no_role_assigned: Sin rol asignado - not_subscribed: No se está suscrito + not_subscribed: No suscrito pending: Revisión pendiente perform_full_suspension: Suspender previous_strikes: Amonestaciones previas @@ -139,10 +141,10 @@ es-MX: search_same_ip: Otros usuarios con la misma IP security: Seguridad security_measures: - only_password: Sólo contraseña + only_password: Solo contraseña password_and_2fa: Contraseña y 2FA sensitive: Sensible - sensitized: marcado como sensible + sensitized: Marcado como sensible shared_inbox_url: URL de bandeja compartida show: created_reports: Reportes hechos por esta cuenta @@ -161,10 +163,10 @@ es-MX: unblocked_email_msg: Desbloqueo exitoso de la dirección de correo de %{username} unconfirmed_email: Correo electrónico sin confirmar undo_sensitized: Desmarcar como sensible - undo_silenced: Des-silenciar - undo_suspension: Des-suspender + undo_silenced: Deshacer límite + undo_suspension: Deshacer suspensión unsilenced_msg: Se quitó con éxito el límite de la cuenta %{username} - unsubscribe: Desuscribir + unsubscribe: Cancelar suscripción unsuspended_msg: Se quitó con éxito la suspensión de la cuenta de %{username} username: Nombre de usuario view_domain: Ver resumen del dominio @@ -312,7 +314,7 @@ es-MX: empty: No se encontraron registros. filter_by_action: Filtrar por acción filter_by_user: Filtrar por usuario - title: Log de auditoría + title: Registro de auditoría unavailable_instance: "(nombre de dominio no disponible)" announcements: back: Volver a la sección de anuncios @@ -359,7 +361,7 @@ es-MX: listed: Listados new: title: Añadir nuevo emoji personalizado - no_emoji_selected: No se cambió ningún emoji ya que no se seleccionó ninguno + no_emoji_selected: No se modificó ningún emoji, ya que no se seleccionó ninguno not_permitted: No tienes permiso para realizar esta acción overwrite: Sobrescribir shortcode: Código de atajo @@ -373,8 +375,8 @@ es-MX: upload: Subir dashboard: active_users: usuarios activos - interactions: interaccciones - media_storage: Almacenamiento + interactions: interacciones + media_storage: Almacenamiento multimedia new_users: nuevos usuarios opened_reports: informes abiertos pending_appeals_html: @@ -413,7 +415,7 @@ es-MX: confirm_suspension: cancel: Cancelar confirm: Suspender - permanent_action: Deshacer la suspensión no recuperará ningún data o relaciones. + permanent_action: Anular la suspensión no restaurará ningún dato ni relación. preamble_html: Estás a punto de suspender a %{domain} y sus subdominios. remove_all_data: Esto eliminará todo el contenido, multimedia y datos de perfil de las cuentas de este dominio de tu servidor. stop_communication: Tu servidor dejará de comunicarse con estos servidores. @@ -436,7 +438,7 @@ es-MX: silence: Limitar suspend: Suspender title: Nuevo bloque de dominio - no_domain_block_selected: No se cambió ningún bloqueo de dominio ya que ninguno fue seleccionado + no_domain_block_selected: No se modificó ningún bloqueo de dominio, ya que no se seleccionó ninguno not_permitted: No tienes permiso para realizar esta acción obfuscate: Ocultar nombre de dominio obfuscate_hint: Oculta parcialmente el nombre de dominio en la lista si mostrar la lista de limitaciones de dominio está habilitado @@ -624,12 +626,12 @@ es-MX: '94670856': 3 años new: title: Crear nueva regla IP - no_ip_block_selected: No se han cambiado reglas IP ya que no se ha seleccionado ninguna + no_ip_block_selected: No se modificó ninguna regla de IP, ya que no se seleccionó ninguna title: Reglas IP relationships: title: Relaciones de %{acct} relays: - add_new: Añadir un nuevo relés + add_new: Añadir nuevo relé delete: Borrar description_html: Un relés de federación es un servidor intermedio que intercambia grandes volúmenes de publicaciones públicas entre servidores que se suscriben y publican en él. Puede ayudar a servidores pequeños y medianos a descubrir contenido del fediverso, que de otra manera requeriría que los usuarios locales siguiesen manualmente a personas de servidores remotos. disable: Deshabilitar @@ -659,7 +661,7 @@ es-MX: mark_as_sensitive_description_html: Los archivos multimedia en las publicaciones reportadas se marcarán como sensibles y se aplicará una amonestación para ayudarte a escalar las futuras infracciones de la misma cuenta. other_description_html: Ver más opciones para controlar el comportamiento de la cuenta y personalizar la comunicación de la cuenta reportada. resolve_description_html: No se tomarán medidas contra la cuenta denunciada, no se registrará la amonestación, y se cerrará el informe. - silence_description_html: La cuenta será visible sólo para aquellos que ya la sigan o la busquen manualmente, limitando severamente su visibilidad. Siempre puede ser revertido. Cierra todos los reportes contra esta cuenta. + silence_description_html: La cuenta será visible solo para aquellos que ya la sigan o la busquen manualmente, limitando severamente su visibilidad. Siempre puede ser revertido. Cierra todos los reportes contra esta cuenta. suspend_description_html: La cuenta y todos sus contenidos serán inaccesibles y eventualmente eliminados, e interactuar con ella será imposible. Reversible durante 30 días. Cierra todos los reportes contra esta cuenta. actions_description_html: Decide qué medidas tomar para resolver esta denuncia. Si tomas una acción punitiva contra la cuenta denunciada, se le enviará a dicha cuenta una notificación por correo electrónico, excepto cuando se seleccione la categoría Spam. actions_description_remote_html: Decide qué medidas tomar para resolver este reporte. Esto solo afectará a la forma en que tu servidor se comunica con esta cuenta remota y gestiona su contenido. @@ -846,6 +848,7 @@ es-MX: publish_statistics: Publicar estadísticas title: Descubrimiento trends: Tendencias + wrapstodon: Wrapstodon domain_blocks: all: A todos disabled: A nadie @@ -2186,4 +2189,5 @@ es-MX: otp_required: Para usar claves de seguridad, por favor habilite primero la autenticación de doble factor. registered_on: Registrado el %{date} wrapstodon: + description: "¡Ve cómo %{name} usó Mastodon este año!" title: Wrapstodon %{year} para %{name} diff --git a/config/locales/es.yml b/config/locales/es.yml index 1dd2760b403567..ccee7cab646819 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -7,6 +7,8 @@ es: hosted_on: Mastodon alojado en %{domain} title: Acerca de accounts: + errors: + cannot_be_added_to_collections: Esta cuenta no se puede añadir a las colecciones. followers: one: Seguidor other: Seguidores @@ -2186,4 +2188,5 @@ es: otp_required: Para usar claves de seguridad, por favor habilite primero la autenticación de doble factor. registered_on: Registrado el %{date} wrapstodon: + description: "¡Mira cómo %{name} ha usado Mastodon este año!" title: Wrapstodon %{year} para %{name} diff --git a/config/locales/eu.yml b/config/locales/eu.yml index f73a1284d889da..f60c4bcce4bb2c 100644 --- a/config/locales/eu.yml +++ b/config/locales/eu.yml @@ -7,6 +7,8 @@ eu: hosted_on: Mastodon %{domain} domeinuan ostatatua title: Honi buruz accounts: + errors: + cannot_be_added_to_collections: Ezin da kontu hau bildumetara gehitu. followers: one: Jarraitzaile other: jarraitzaile @@ -190,6 +192,7 @@ eu: create_relay: Erreleboa sortu create_unavailable_domain: Sortu eskuragarri ez dagoen domeinua create_user_role: Sortu rola + create_username_block: Sortu erabiltzaile-izenaren araua demote_user: Jaitsi erabiltzailearen maila destroy_announcement: Ezabatu iragarpena destroy_canonical_email_block: Ezabatu email blokeoa @@ -203,12 +206,14 @@ eu: destroy_status: Ezabatu bidalketa destroy_unavailable_domain: Ezabatu eskuragarri ez dagoen domeinua destroy_user_role: Ezabatu rola + destroy_username_block: Ezabatu erabiltzaile-izenaren araua disable_2fa_user: Desgaitu 2FA disable_custom_emoji: Desgaitu emoji pertsonalizatua disable_relay: Erreleboa desaktibatu disable_sign_in_token_auth_user: Desgaitu email token autentifikazioa erabiltzailearentzat disable_user: Desgaitu erabiltzailea enable_custom_emoji: Gaitu emoji pertsonalizatua + enable_relay: Gaitu errelea enable_sign_in_token_auth_user: Gaitu email token autentifikazioa erabiltzailearentzat enable_user: Gaitu erabiltzailea memorialize_account: Bihurtu kontua oroigarri @@ -236,6 +241,7 @@ eu: update_report: Txostena eguneratu update_status: Eguneratu bidalketa update_user_role: Eguneratu rola + update_username_block: Eguneratu erabiltzaile-izenaren araua actions: approve_appeal_html: "%{name} erabiltzaileak %{target} erabiltzailearen moderazio erabakiaren apelazioa onartu du" approve_user_html: "%{name} erabiltzaileak %{target} erabiltzailearen izen-ematea onartu du" @@ -310,7 +316,9 @@ eu: create: Sortu iragarpena title: Iragarpen berria preview: + disclaimer: Erabiltzaileek ezin dutenez horiei uko egin, posta elektroniko bidezko jakinarazpenak iragarpen garrantzitsuetara mugatu behar dira; adibidez, datu pertsonalen segurtasun-haustura edo zerbitzariaren itxiera jakinarazpenetara. explanation_html: 'Mezu elektronikoa %{display_count} erabiltzaileei bidaliko zaie. Testu hau gehituko zaio mezu elektronikoari:' + title: Aurreikusi iragarpenaren aurrebista publish: Argitaratu published_msg: Iragarpena ongi argitaratu da! scheduled_for: "%{time}-rako programatuta" @@ -441,6 +449,7 @@ eu: attempts_over_week: one: Izen-emateko saiakera %{count} azken astean other: Izen-emateko %{count} saiakera azken astean + created_msg: Ongi gehitu da e-mail helbidea domeinuen zerrenda beltzera delete: Ezabatu dns: types: @@ -450,7 +459,9 @@ eu: create: Gehitu domeinua resolve: Ebatzi domeinua title: Posta domeinu berria blokeatu + no_email_domain_block_selected: Ez da eposta domeinu blokeorik aldatu ez delako bat ere hautatu not_permitted: Baimendu gabea + resolved_dns_records_hint_html: Domeinu-izena ondorengo MX domeinuetara ebazten da, zeinek eposta onartzeko ardura duten. MX domeinu bat blokeatzeak MX domeinu hori erabiltzen duen edozein helbide elektronikotatik izena-ematea blokeatzen du, baita ikusgai dagoen domeinu-izena beste bat bada ere. Kontuz ibili eposta hornitzaile nagusiak blokeatu gabe. resolved_through_html: "%{domain} domeinuaren bidez ebatzia" title: Email domeinua blokeatuta export_domain_allows: @@ -479,6 +490,7 @@ eu: providers: active: Aktibo base_url: Oinarrizko URL-a + callback: Dei-erantzuna delete: Ezabatu edit: Editatu hornitzailea finish_registration: Izen-ematea bukatu @@ -488,8 +500,11 @@ eu: registration_requested: Izen-ematea eskatuta registrations: confirm: Berretsi + description: Erregistro bat jaso duzu FASP batetik. Ukatu ez baduzu zuk zeuk abiarazi. Zuk abiarazi baduzu, alderatu arretaz izena eta gakoaren hatz-marka erregistroa berretsi aurretik. reject: Ukatu + title: Berretsi FASP erregistroa save: Gorde + select_capabilities: Hautatu ahalmenak sign_in: Hasi saioa status: Egoera title: Fedibertsoko zerbitzu osagarrien hornitzaileak @@ -503,6 +518,9 @@ eu: title: Jarraitzeko gomendioak unsuppress: Berrezarri jarraitzeko gomendioa instances: + audit_log: + title: Oraintsuko ikuskapen-erregistroak + view_all: Ikusi ikuskapen-erregistro osoak availability: description_html: one: Domeinura entregatzeak arrakastarik gabe huts egiten badu egun %{count} igaro ondoren, ez da entregatzeko saiakera gehiago egingo, ez bada domeinu horretatik entregarik jasotzen. @@ -563,6 +581,8 @@ eu: create: Gehitu Moderazio Oharra created_msg: Instantziako moderazio oharra ongi sortu da! description_html: Ikusi eta idatzi oharrak beste moderatzaileentzat eta zuretzat etorkizunerako + destroyed_msg: Instantziako moderazio oharra ongi ezabatu da! + placeholder: Instantzia honi buruzko informazioa, burututako ekintzak edo etorkizunean instantzia hau moderatzen lagunduko dizun beste edozein gauza. title: Moderazio oharrak private_comment: Iruzkin pribatua public_comment: Iruzkin publikoa @@ -634,7 +654,9 @@ eu: resolve_description_html: Ez da ekintzarik hartuko salatutako kontuaren aurka, ez da neurria gordeko eta salaketa itxiko da. silence_description_html: Kontua soilik honen jarraitzaile edo espresuki bilatzen dutenentzat izango da ikusgarri, kontuaren irisgarritasuna gogorki mugatzen delarik. suspend_description_html: Kontua bera eta honen edukiak eskuraezinak izango dira, eta azkenean, ezabatuak. Kontu honekin erlazionatzea ezinezkoa izango da. Prozesua 30 egunez itzulgarria izango da. Kontu honen aurkako txosten guztiak baztertuko lirateke. + actions_description_html: Erabaki txosten hau konpontzeko ze ekintza hartu. Salatutako kontuaren aurka zigor ekintza bat hartzen baduzu, eposta jakinarazpen bat bidaliko zaie, Spam kategoria hautatzean ezik. actions_description_remote_html: Hautatu txosten honi konponbidea aurkitzeko zein neurri hartu. Hau soilik zure zerbitzaria urruneko kontu honekin nola komunikatu eta bere edukia nola maneiatzeko da. + actions_no_posts: Txosten honek ez du ezabatzeko lotutako argitalpenik add_to_report: Gehitu gehiago txostenera already_suspended_badges: local: Dagoeneko kanporatu da zerbitzari honetatik @@ -675,6 +697,7 @@ eu: report: 'Salaketa #%{id}' reported_account: Salatutako kontua reported_by: Salatzailea + reported_with_application: Aplikazioaren bidez salatua resolved: Konponduta resolved_msg: Salaketa ongi konpondu da! skip_to_actions: Salto ekintzetara @@ -737,6 +760,7 @@ eu: manage_appeals: Kudeatu apelazioak manage_appeals_description: Baimendu erabiltzaileek moderazio ekintzen aurkako apelazioak berrikustea manage_blocks: Kudeatu blokeatzeak + manage_blocks_description: Baimendu erabiltzaileek eposta hornitzaile eta IP helbideak blokeatzea manage_custom_emojis: Kudeatu emoji pertsonalizatuak manage_custom_emojis_description: Baimendu erabiltzaileek zerbitzariko emoji pertsonalizatuak kudeatzea manage_federation: Kudeatu federazioa @@ -754,6 +778,7 @@ eu: manage_taxonomies: Kudeatu taxonomiak manage_taxonomies_description: Baimendu erabiltzaileek joerak berrikustea eta traolen ezarpenak eguneratzea manage_user_access: Kudeatu erabiltzaileen sarbidea + manage_user_access_description: Baimendu erabiltzaileek beste erabiltzaileen bi faktoreko autentifikazioa desaktibatzea, eposta helbideak aldatzea eta pasahitzak berrezartzea manage_users: Kudeatu erabiltzaileak manage_users_description: Baimendu erabiltzaileek beste erabiltzaileen xehetasunak ikusi eta moderazio ekintzak burutzea manage_webhooks: Kudeatu webhook-ak @@ -801,18 +826,22 @@ eu: title: Erabiltzaileei bilaketa-motorraren indexaziotik at egoteko aukera ematen die lehenetsitako aukera modura discovery: follow_recommendations: Jarraitzeko gomendioak + preamble: Eduki interesgarria aurkitzea garrantzitsua da Mastodoneko erabiltzaile berrientzat, behar bada inor ez dutelako ezagutuko. Kontrolatu zure zerbitzariko aurkikuntza-ezaugarriek nola funtzionatzen duten. privacy: Pribatutasuna profile_directory: Profil-direktorioa public_timelines: Denbora-lerro publikoak publish_statistics: Argitaratu estatistikak title: Aurkitzea trends: Joerak + wrapstodon: Wrapstodon domain_blocks: all: Guztiei disabled: Inori ez users: Saioa hasita duten erabiltzaile lokalei feed_access: modes: + authenticated: Saioa hasi duten erabiltzaileentzat soilik + disabled: Erabiltzaile-rol jakin bat behar da public: Edonork landing_page: values: @@ -942,8 +971,10 @@ eu: not_trendable: Ez dago modan not_usable: Ez erabilgarri pending_review: Berrikusketaren zain + review_requested: Berrikuspena eskatuta reviewed: Berrikusita title: Egoera + trendable: Joera bihur daiteke unreviewed: Berrikusi gabe usable: Erabilgarri name: Izena @@ -969,6 +1000,9 @@ eu: going_live_on_html: Argitaratua, indarrean %{date} history: Historia live: Zuzenean + notify_users: Jakinarazi erabiltzaileak + preview: + send_preview: 'Bidali aurrebista hona: %{email}' publish: Argitaratu published_on_html: "%{date}(e)an argitaratua" save_draft: Gorde zirriborroa @@ -977,11 +1011,14 @@ eu: trends: allow: Onartu approved: Onartua + confirm_allow: Ziur zaude hautatutako etiketak gaitu nahi dituzula? confirm_disallow: Ziur zaude hautatutako etiketak desgaitu nahi dituzula? disallow: Ukatu links: allow: Onartu esteka allow_provider: Onartu argitaratzailea + confirm_allow: Ziur zaude hautatutako estekak gaitu nahi dituzula? + confirm_disallow: Ziur zaude hautatutako estekak desgaitu nahi dituzula? description_html: Esteka hauek zure zerbitzariak ikusten dituen kontuek asko zabaltzen ari diren estekak dira. Zure erabiltzaileei munduan ze berri den jakiteko lagungarriak izan daitezke. Ez da estekarik bistaratzen argitaratzaileak onartu arte. Esteka bakoitza onartu edo baztertu dezakezu. disallow: Ukatu esteka disallow_provider: Ukatu argitaratzailea @@ -1037,9 +1074,23 @@ eu: used_by_over_week: one: Pertsona batek erabilia azken astean other: "%{count} pertsonak erabilia azken astean" + title: Gomendioak eta joerak trending: Joerak username_blocks: add_new: Gehitu berria + block_registrations: Blokeatu izen-emateak + comparison: + equals: Berdin + delete: Ezabatu + edit: + title: Editatu erabiltzaile-izena araua + matches_exactly_html: 'Berdin: %{string}' + new: + create: Sortu araua + title: Sortu erabiltzaile-izen araua berria + no_username_block_selected: Ez da erabiltzaile-izen araurik aldatu, ez delako bat ere hautatu + not_permitted: Baimendu gabea + title: Erabiltzaile-izen arauak warning_presets: add_new: Gehitu berria delete: Ezabatu @@ -1111,6 +1162,7 @@ eu: hint_html: Beste kontu batetik hona migratu nahi baduzu, hemen ezizen bat sortu dezakezu, hau beharrezkoa da kontu zaharreko jarraitzaileak hona ekartzeko. Ekintza hau berez kaltegabea eta desegingarria da. Kontuaren migrazioa kontu zaharretik abiatzen da. remove: Deslotu ezizena appearance: + advanced_settings: Ezarpen aurreratuak animations_and_accessibility: Animazioak eta irisgarritasuna discovery: Aurkitzea localization: @@ -1119,6 +1171,7 @@ eu: guide_link_text: Edonork lagundu dezake. sensitive_content: Eduki hunkigarria application_mailer: + notification_preferences: Posta elektronikoaren lehentasunak aldatu salutation: "%{name}," settings: 'Posta elektronikoaren lehentasunak aldatu: %{link}' unsubscribe: Kendu harpidetza @@ -1140,6 +1193,7 @@ eu: hint_html: Azken kontu bat! Gizakia zarela berretsi behar dugu (zabor-kontuak kanpoan mantentzeko baino ez da!) Ebatzi azpiko CAPTCHA eta sakatu "Jarraitu". title: Segurtasun txekeoa confirmations: + awaiting_review: Zure helbide elektronikoa baieztatu da! %{domain} lan taldea zure erregistroa berrikusten ari da. Mezu elektroniko bat jasoko duzu zure kontua onartzen badute! awaiting_review_title: Zure izen-ematea berrikusten ari da clicking_this_link: lotura hau klikatzen login_link: hasi saioa @@ -1147,6 +1201,7 @@ eu: redirect_to_app_html: "%{app_name} aplikaziora berbideratua izan beharko zenuke. Hori gertatu ez bada, saiatu %{clicking_this_link} edo eskuz itzuli." registration_complete: Osatuta dago orain zure izen-ematea %{domain} -en! welcome_title: Ongi etorri, %{name}! + wrong_email_hint: Helbide-elektroniko hori zuzena ez bada, kontuaren ezarpenetan alda dezakezu. delete_account: Ezabatu kontua delete_account_html: Kontua ezabatu nahi baduzu, jarraitu hemen. Berrestea eskatuko zaizu. description: @@ -1197,9 +1252,11 @@ eu: title: "%{domain}-(e)an saioa hasi" sign_up: manual_review: "%{domain}-(e)n eginiko izen-emateak gure moderatzaileek eskuz aztertzen dituzte. Zure izen-ematea prozesatzen lagun gaitzazun, idatz ezazu zertxobait zuri buruz eta azaldu zergatik nahi duzun %{domain}-(e)n kontu bat." + preamble: Mastodon zerbitzari honetako kontu batekin, aukera izango duzu fedibertsoko edozein pertsona jarraitzeko, ez dio axola kontua non ostatatua dagoen. title: "%{domain} zerbitzariko kontua prestatuko dizugu." status: account_status: Kontuaren egoera + confirming: E-mail baieztapena osatu bitartean zain. functional: Zure kontua guztiz erabilgarri dago. pending: Gure taldea zure eskaera berrikusten ari da. Honek denbora pixka bat beharko du. Mezu elektroniko bat jasoko duzu zure eskaera onartzen bada. redirecting_to: Zure kontua ez dago aktibo orain %{acct} kontura birbideratzen duelako. @@ -1207,6 +1264,9 @@ eu: view_strikes: Ikusi zure kontuaren aurkako neurriak too_fast: Formularioa azkarregi bidali duzu, saiatu berriro. use_security_key: Erabili segurtasun gakoa + author_attribution: + example_title: Testu-lagina + more_from_html: "%{name} erabiltzaileaz gehiago jakin" challenge: confirm: Jarraitu hint_html: "Oharra: Ez dizugu pasahitza berriro eskatuko ordu batez." @@ -1243,6 +1303,7 @@ eu: before: 'Jarraitu aurretik, irakurri adi ohar hauek:' caches: Beste zerbitzariek cachean duten edukia mantentzea gerta daiteke data_removal: Zure bidalketak eta beste datuak behin betiko ezabatuko dira + email_reconfirmation_html: Ez baduzu baieztamen e-maila jasotzen, berriro eskatu dezakezu irreversible: Ezin izango duzu kontua berreskuratu edo berraktibatu more_details_html: Xehetasun gehiagorako, ikusi pribatutasun politika. username_available: Zure erabiltzaile-izena berriro eskuragarri egongo da @@ -1281,6 +1342,10 @@ eu: basic_information: Oinarrizko informazioa hint_html: "Pertsonalizatu jendeak zer ikusi dezakeen zure profil publikoan eta zure bidalketen baitan. Segur aski, jende gehiagok jarraituko dizu eta interakzio gehiago izango dituzu profila osatuta baduzu, profil irudia eta guzti." other: Bestelakoak + emoji_styles: + auto: Automatikoa + native: Bertakoa + twemoji: Twemoji errors: '400': Bidali duzun eskaera baliogabea da edo gaizki osatua dago. '403': Ez duzu orri hau ikusteko baimenik. @@ -1452,6 +1517,13 @@ eu: expires_at: Iraungitzea uses: Erabilerak title: Gonbidatu jendea + link_preview: + author_html: 'Egilea: %{name}' + potentially_sensitive_content: + action: Egin klik erakusteko + confirm_visit: Ziur zaude esteka hau ireki nahi duzula? + hide_button: Ezkutatu + label: Eduki sentikorra izan daiteke lists: errors: limit: Gehienezko zerrenda kopurura iritsi zara @@ -1459,6 +1531,7 @@ eu: authentication_methods: otp: bi faktoreko autentifikazio aplikazioa password: pasahitza + sign_in_token: e-posta segurtasun kodea webauthn: segurtasun gakoak description_html: Ezagutzen ez duzun aktibitatea ikusten baduzu, pasahitza aldatu eta bi faktoreko autentifikazioa gaitzea gomendatzen dizugu. empty: Ez dago autentifikazio historiarik eskuragarri @@ -1469,9 +1542,18 @@ eu: unsubscribe: action: Bai, kendu harpidetza complete: Harpidetza kenduta + confirmation_html: |- + Ziur Mastodonen %{domain} zerbitzariko %{type} %{email} helbide elektronikoan jasotzeari utzi nahi diozula? + Beti harpidetu zaitezke berriro eposta jakinarazpenen hobespenetan. emails: notification_emails: + favourite: zure argitalpena gogoko egin dutenaren jakinarazpen e-mailak follow: jarraitu jakinarazpen-mezu elektronikoak + follow_request: jarraipen-eskaeren jakinarazpen e-mailak + mention: aipamenen jakinarazpen e-mailak + reblog: bultzaden jakinarazpen e-mailak + resubscribe_html: Nahi gabe utzi badiozu jakinarazpenak jasotzeari, berriro harpidetu zaitezke e-mail jakinarazpenen hobespenetan. + success_html: Ez duzu Mastodonen %{domain} zerbitzariko %{type} jasoko %{email} helbide elektronikoan. title: Kendu harpidetza media_attachments: validations: @@ -1507,6 +1589,7 @@ eu: disabled_account: Zure uneko kontua ezin izango da gero erabili. Hala ere, datua exporatu ahal izango dituzu, eta berriro aktibatu. followers: Ekintza honek jarraitzaile guztiak eramango ditu uneko kontutik kontu berrira only_redirect_html: Bestela, zure profilean birbideratze bat jar dezakezu. + other_data: Ez da beste daturik automatikoki mugituko (zure argitalpenak eta jarraitzen dituzun kontuen zerrenda barne) redirect: Zure uneko kontuaren profila eguneratuko da birbideratze ohar batekin eta bilaketetatik kenduko da moderation: title: Moderazioa @@ -1540,17 +1623,29 @@ eu: body: "%{name}(e)k aipatu zaitu:" subject: "%{name}(e)k aipatu zaitu" title: Aipamen berria + moderation_warning: + subject: Moderazio-abisu bat jaso duzu poll: subject: "%{name} erabiltzailearen inkesta bat amaitu da" + quote: + body: "%{name}(e)k zure bidalketari aipamena egin dio:" + subject: "%{name} erabiltzaileak zure bidalketa aipatu du" + title: Aipamen berria + quoted_update: + subject: "%{name} erabiltzaileak aipatu duzun post bat editatu du" reblog: body: "%{name}(e)k bultzada eman dio zure bidalketari:" subject: "%{name}(e)k bultzada eman dio zure bidalketari" title: Bultzada berria + severed_relationships: + subject: Harremanak galdu dituzu moderazio-erabaki baten ondorioz status: subject: "%{name} erabiltzaileak bidalketa egin berri du" update: subject: "%{name} erabiltzaileak bidalketa bat editatu du" notifications: + administration_emails: Administratzailearen posta elektroniko bidezko jakinarazpenak + email_events: E-mail jakinarazpenentzako gertaerak email_events_hint: 'Hautatu jaso nahi dituzun gertaeren jakinarazpenak:' number: human: @@ -1588,6 +1683,9 @@ eu: self_vote: Ezin duzu zuk sortutako inkestetan bozka eman too_few_options: elementu bat baino gehiago izan behar du too_many_options: ezin ditu %{max} elementu baino gehiago izan + vote: Bozkatu + posting_defaults: + explanation: Konfigurazio hau lehenetsi gisa erabiliko da argitalpen berriak sortzen dituzunean, baina aldatu egin dezakezu argitalpen bat idaztean. preferences: other: Denetarik posting_defaults: Bidalketarako lehenetsitakoak @@ -1644,6 +1742,7 @@ eu: scheduled_statuses: over_daily_limit: 'Egun horretarako programatutako bidalketa kopuruaren muga gainditu duzu: %{limit}' over_total_limit: 'Programatutako bidalketa kopuruaren muga gainditu duzu: %{limit}' + too_soon: datak etorkizunekoa izan behar du self_destruct: lead_html: Zoritxarrez, %{domain} betirako itxiko da. Kontu bat baduzu bertan, ezin izango duzu erabiltzen jarraitu, baina, oraindik zure datuen babeskopia bat eska dezakezu. title: Zerbitzari hau ixtear dago @@ -1742,6 +1841,9 @@ eu: other: "%{count} bideo" boosted_from_html: "%{acct_link}(e)tik bultzatua" content_warning: 'Edukiaren abisua: %{warning}' + content_warnings: + hide: Tuta ezkutatu + show: Erakutsi gehiago default_language: Interfazearen hizkuntzaren berdina disallowed_hashtags: one: 'debekatutako traola bat zuen: %{tags}' @@ -1749,15 +1851,31 @@ eu: edited_at_html: Editatua %{date} errors: in_reply_not_found: Erantzuten saiatu zaren bidalketa antza ez da existitzen. + quoted_status_not_found: Aipua egiten saiatu zaren bidalketa antza ez da existitzen. + quoted_user_not_mentioned: Ezin da aipatu ez den erabiltzaile baten aipamenik egin Aipamen pribatu batean. over_character_limit: "%{max}eko karaktere muga gaindituta" pin_errors: direct: Aipatutako erabiltzaileentzat soilik ikusgai dauden bidalketak ezin dira finkatu limit: Gehienez finkatu daitekeen bidalketa kopurua finkatu duzu jada ownership: Ezin duzu beste norbaiten bidalketa bat finkatu reblog: Bultzada bat ezin da finkatu + quote_error: + not_available: Bidalketa ez dago eskuragarri + pending_approval: Bidalketa zain dago + revoked: Egileak bidalketa kendu du + quote_policies: + followers: Jarraitzaileentzat soilik + nobody: Nik bakarrik + public: Edonork + quote_post_author: "%{acct}-(r)en bidalketan aipatua" title: '%{name}: "%{quote}"' visibilities: + direct: Aipu pribatua + private: Jarraitzaileentzat soilik public: Publikoa + public_long: Mastodonen dagoen edo ez dagoen edonor + unlisted: Ikusgarritasun mugatua + unlisted_long: Ezkutatuta Mastodon bilaketen emaitzetatik, joeretatik, eta denbora-lerro publikoetatik statuses_cleanup: enabled: Ezabatu bidalketa zaharrak automatikoki enabled_hint: Zure bidalketa zaharrak automatikoki ezabatzen ditu zehazturiko denbora mugara iristean, beheko baldintza bat betetzen ez bada @@ -1804,6 +1922,8 @@ eu: title: Erabilera baldintzak terms_of_service_interstitial: future_preamble_html: Gure zerbitzu-baldintzetan aldaketa batzuk egiten ari gara, eta %{date}-tik aurrera jarriko dira indarrean. Eguneratutako baldintzak berrikustea gomendatzen dizugu. + past_preamble_html: Zerbitzu-baldintzak aldatu ditugu zure azken bisitatik. Baldintza eguneratuak berrikustera animatzen zaitugu. + review_link: Berrikusi zerbitzu-baldintzak themes: contrast: Mastodon (Kontraste altua) default: Mastodon (Iluna) @@ -1836,6 +1956,7 @@ eu: webauthn: Segurtasun gakoak user_mailer: announcement_published: + subject: Zerbitzuaren iragarpena title: "%{domain} zerbitzuaren iragarpena" appeal_approved: action: Kontuaren ezarpenak @@ -1866,6 +1987,12 @@ eu: further_actions_html: Ez bazara zu izan, lehenbailehen %{action} gomendatzen dizugu eta bi faktoreko autentifikazioa gaitzea zure kontua seguru mantentzeko. subject: Zure kontura sarbidea egon da IP helbide berri batetik title: Saio hasiera berria + terms_of_service_changed: + changelog: 'Laburbilduz, hau da eguneratze honek zuretzat esan nahi duena:' + sign_off: "%{domain} taldea" + subject: Zerbitzu-baldintzen eguneratzeak + subtitle: "%{domain}(e)ko zerbitzu-baldintzak aldatu egingo dira" + title: Eguneraketa garrantzitsua warning: appeal: Bidali apelazioa appeal_description: Hau errore bat dela uste baduzu, apelazio bat bidali diezaiekezu %{instance} instantziako arduradunei. @@ -1943,6 +2070,7 @@ eu: invalid_otp_token: Bi faktoreetako kode baliogabea otp_lost_help_html: 'Bietara sarbidea galdu baduzu, jarri kontaktuan hemen: %{email}' rate_limited: Autentifikazio saiakera gehiegi, saiatu berriro geroago. + seamless_external_login: Kanpo zerbitzu baten bidez hasi duzu saioa, beraz pasahitza eta e-mail ezarpenak ez daude eskuragarri. signed_in_as: 'Saioa honela hasita:' verification: extra_instructions_html: Aholkua: webguneko esteka ikusezina izan daiteke. Muina rel="me" da, erabiltzaileak sortutako edukia duten webguneetan beste inor zure burutzat aurkeztea eragozten duena. a beharrean esteka motako etiketa bat ere erabil dezakezu orriaren goiburuan, baina HTMLak erabilgarri egon behar du JavaScript exekutatu gabe. @@ -1969,3 +2097,6 @@ eu: not_supported: Nabigatzaile honek ez ditu segurtasun gakoak onartzen otp_required: Segurtasun gakoak erabili aurretik bi faktoreko autentifikazioa gaitu behar duzu. registered_on: "%{date}(e)an erregistratua" + wrapstodon: + description: Begira nola erabili duen Mastodon %{name}(e)k urte honetan! + title: "%{year}(e)ko Wrapstodona, %{name}(e)rentzat" diff --git a/config/locales/fi.yml b/config/locales/fi.yml index 8375136962f248..eee902e812ff5c 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -7,6 +7,8 @@ fi: hosted_on: Mastodon palvelimella %{domain} title: Tietoja accounts: + errors: + cannot_be_added_to_collections: Tätä tiliä ei voi lisätä kokoelmiin. followers: one: seuraaja other: seuraajaa @@ -846,6 +848,7 @@ fi: publish_statistics: Julkaise tilastot title: Löydettävyys trends: Trendit + wrapstodon: Wrapstodon domain_blocks: all: Kaikille disabled: Ei kenellekään @@ -2185,3 +2188,6 @@ fi: not_supported: Tämä selain ei tue suojausavaimia otp_required: Jos haluat käyttää suojausavaimia, ota ensin kaksivaiheinen todennus käyttöön. registered_on: Rekisteröity %{date} + wrapstodon: + description: Katso, kuinka %{name} käytti Mastodonia tänä vuonna! + title: Käyttäjän %{name} Wrapstodon vuodelle %{year} diff --git a/config/locales/fo.yml b/config/locales/fo.yml index a94f0d1feda343..4216367aef5efa 100644 --- a/config/locales/fo.yml +++ b/config/locales/fo.yml @@ -7,6 +7,8 @@ fo: hosted_on: Mastodon hýst á %{domain} title: Um accounts: + errors: + cannot_be_added_to_collections: Hendan kontan kann ikki leggjast afturat søvnum. followers: one: Fylgjari other: Fylgjarar @@ -2186,4 +2188,5 @@ fo: otp_required: Fyri at brúka trygdarlyklar er neyðugt at gera váttan í tveimum stigum virkna fyrst. registered_on: Skrásett %{date} wrapstodon: + description: Sí hvussu %{name} brúkti Mastodon í ár! title: Wrapstodon %{year} fyri %{name} diff --git a/config/locales/fr-CA.yml b/config/locales/fr-CA.yml index 78e0d4263a42c3..6590c544d9fa4f 100644 --- a/config/locales/fr-CA.yml +++ b/config/locales/fr-CA.yml @@ -7,6 +7,8 @@ fr-CA: hosted_on: Serveur Mastodon hébergé sur %{domain} title: À propos accounts: + errors: + cannot_be_added_to_collections: Ce compte ne peut pas être ajouté aux collections. followers: one: Abonné·e other: Abonné·e·s @@ -849,6 +851,7 @@ fr-CA: publish_statistics: Publier les statistiques title: Découverte trends: Tendances + wrapstodon: Wrapstodon domain_blocks: all: À tout le monde disabled: À personne @@ -2189,4 +2192,5 @@ fr-CA: otp_required: Pour utiliser les clés de sécurité, veuillez d'abord activer l'authentification à deux facteurs. registered_on: Inscrit le %{date} wrapstodon: + description: Voir comment %{name} a utilisé Mastodon cette année ! title: Wrapstodon %{year} pour %{name} diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 0767c223bb4262..3ce00ca9f97a7e 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -7,6 +7,8 @@ fr: hosted_on: Serveur Mastodon hébergé sur %{domain} title: À propos accounts: + errors: + cannot_be_added_to_collections: Ce compte ne peut pas être ajouté aux collections. followers: one: Abonné·e other: Abonné·e·s @@ -849,6 +851,7 @@ fr: publish_statistics: Publier les statistiques title: Découverte trends: Tendances + wrapstodon: Wrapstodon domain_blocks: all: À tout le monde disabled: À personne @@ -2189,4 +2192,5 @@ fr: otp_required: Pour utiliser les clés de sécurité, veuillez d'abord activer l'authentification à deux facteurs. registered_on: Inscrit le %{date} wrapstodon: + description: Voir comment %{name} a utilisé Mastodon cette année ! title: Wrapstodon %{year} pour %{name} diff --git a/config/locales/ga.yml b/config/locales/ga.yml index 9c5ea95f847753..acba6eab84944c 100644 --- a/config/locales/ga.yml +++ b/config/locales/ga.yml @@ -7,6 +7,8 @@ ga: hosted_on: Mastodon arna óstáil ar %{domain} title: Maidir le accounts: + errors: + cannot_be_added_to_collections: Ní féidir an cuntas seo a chur le bailiúcháin. followers: few: Leantóirí many: Leantóirí @@ -888,6 +890,7 @@ ga: publish_statistics: Staitisticí a fhoilsiú title: Fionnachtain trends: Treochtaí + wrapstodon: Wrapstodon domain_blocks: all: Do chách disabled: Do dhuine ar bith @@ -2320,4 +2323,5 @@ ga: otp_required: Chun eochracha slándála a úsáid cumasaigh fíordheimhniú dhá fhachtóir ar dtús. registered_on: Cláraithe ar %{date} wrapstodon: + description: Féach conas a d'úsáid %{name} Mastodon i mbliana! title: Wrapstodon %{year} do %{name} diff --git a/config/locales/gl.yml b/config/locales/gl.yml index b9c4ba5427d33e..c465f3fd87d2c8 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -7,6 +7,8 @@ gl: hosted_on: Mastodon aloxado en %{domain} title: Sobre accounts: + errors: + cannot_be_added_to_collections: Non se pode engadir esta conta ás coleccións. followers: one: Seguidora other: Seguidoras @@ -846,6 +848,7 @@ gl: publish_statistics: Publicar estatísticas title: Descubrir trends: Tendencias + wrapstodon: Wrapstodon domain_blocks: all: Para todos disabled: Para ninguén @@ -2186,4 +2189,5 @@ gl: otp_required: Para usar chaves de seguridade tes que activar primeiro o segundo factor. registered_on: Rexistrado o %{date} wrapstodon: + description: Mira como usou %{name} Mastodon este ano! title: Wrapstodon %{year} de %{name} diff --git a/config/locales/he.yml b/config/locales/he.yml index 6550f17cf5a16e..f22d5d96074430 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -7,6 +7,8 @@ he: hosted_on: מסטודון שיושב בכתובת %{domain} title: אודות accounts: + errors: + cannot_be_added_to_collections: לא ניתן להוסיף חשבון זה לאוספים. followers: many: עוקבים one: עוקב @@ -874,6 +876,7 @@ he: publish_statistics: פרסום הסטטיסטיקות בפומבי title: תגליות trends: נושאים חמים + wrapstodon: סיכומודון domain_blocks: all: לכולם disabled: לאף אחד @@ -2274,4 +2277,5 @@ he: otp_required: על מנת להשתמש במפתחות אבטחה אנא אפשר.י אימות דו-שלבי קודם. registered_on: נרשם ב %{date} wrapstodon: + description: ראו איך %{name} השתמשו במסטודון השנה! title: סיכומודון %{year} עבור %{name} diff --git a/config/locales/hu.yml b/config/locales/hu.yml index abaa78e1e43222..3a5172e05cc2a9 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -7,6 +7,8 @@ hu: hosted_on: "%{domain} Mastodon-kiszolgáló" title: Névjegy accounts: + errors: + cannot_be_added_to_collections: Ez a fiók nem adható hozzá gyűjteményekhez. followers: one: Követő other: Követő @@ -846,6 +848,7 @@ hu: publish_statistics: Statisztikák közzététele title: Felfedezés trends: Trendek + wrapstodon: Wrapstodon domain_blocks: all: Mindenkinek disabled: Senkinek @@ -2185,3 +2188,6 @@ hu: not_supported: Ez a böngésző nem támogatja a biztonsági kulcsokat otp_required: A biztonsági kulcsok használatához először engedélyezd a kétlépcsős hitelesítést. registered_on: 'Regisztráció ekkor: %{date}' + wrapstodon: + description: Nézd meg, hogy %{name} hogyan használta a Mastodont az éven! + title: Wrapstodon %{year} – %{name} diff --git a/config/locales/is.yml b/config/locales/is.yml index f964c1ad2ee3ab..d03b74391f7135 100644 --- a/config/locales/is.yml +++ b/config/locales/is.yml @@ -7,6 +7,8 @@ is: hosted_on: Mastodon hýst á %{domain} title: Um hugbúnaðinn accounts: + errors: + cannot_be_added_to_collections: Þessum aðgangi er ekki hægt að bæta í söfn. followers: one: fylgjandi other: fylgjendur @@ -848,6 +850,7 @@ is: publish_statistics: Birta tölfræði title: Uppgötvun trends: Vinsælt + wrapstodon: Ársuppgjörið domain_blocks: all: Til allra disabled: Til engra @@ -2190,4 +2193,5 @@ is: otp_required: Til að nota öryggislykla skaltu fyrst virkja tveggja-þátta auðkenningu. registered_on: Skráði sig %{date} wrapstodon: - title: Wrapstodon %{year} fyrir %{name} + description: Sjáðu hvernig %{name} notaði Mastodon á árinu! + title: Ársuppgjörið %{year} fyrir %{name} diff --git a/config/locales/it.yml b/config/locales/it.yml index 1be6d50ad3da4a..37b131c4b99684 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -7,6 +7,8 @@ it: hosted_on: Mastodon ospitato su %{domain} title: Info accounts: + errors: + cannot_be_added_to_collections: Questo account non può essere aggiunto alle collezioni. followers: one: Seguace other: Seguaci @@ -846,6 +848,7 @@ it: publish_statistics: Pubblica le statistiche title: Scopri trends: Tendenze + wrapstodon: Wrapstodon domain_blocks: all: A tutti disabled: A nessuno @@ -2186,4 +2189,5 @@ it: otp_required: Per utilizzare le chiavi di sicurezza, prima abilita l'autenticazione a due fattori. registered_on: Registrato il %{date} wrapstodon: + description: Guarda come %{name} ha utilizzato Mastodon quest'anno! title: Wrapstodon %{year} per %{name} diff --git a/config/locales/ko.yml b/config/locales/ko.yml index 4f2e8a538502dd..36dabb67cf5b09 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -7,6 +7,8 @@ ko: hosted_on: "%{domain}에서 호스팅 되는 마스토돈" title: 정보 accounts: + errors: + cannot_be_added_to_collections: 이 계정은 컬렉션에 추가할 수 없습니다. followers: other: 팔로워 following: 팔로잉 @@ -2136,3 +2138,5 @@ ko: not_supported: 이 브라우저는 보안 키를 지원하지 않습니다 otp_required: 보안 키를 사용하기 위해서는 2단계 인증을 먼저 활성화 해 주세요 registered_on: "%{date}에 등록됨" + wrapstodon: + title: "%{name} 님의 %{year} 랩스토돈" diff --git a/config/locales/nan.yml b/config/locales/nan.yml index 34c702a714ab1e..e51b8f506b4d63 100644 --- a/config/locales/nan.yml +++ b/config/locales/nan.yml @@ -7,6 +7,8 @@ nan: hosted_on: 佇 %{domain} 運作 ê Mastodon站 title: 關係本站 accounts: + errors: + cannot_be_added_to_collections: Tsit ê口座袂當加入kàu集合。 followers: other: 跟tuè ê following: Leh跟tuè @@ -1488,6 +1490,34 @@ nan: overwrite_preambles: blocking_html: other: Lí teh-beh用 %{filename} ê %{count} ê口座替換lí ê封鎖列單。 + bookmarks_html: + other: Lí teh-beh用 %{filename} ê %{count} ê PO文替換lí ê冊籤。 + domain_blocking_html: + other: Lí teh-beh用 %{filename} ê %{count} ê域名替換lí ê域名封鎖列單。 + following_html: + other: Lí當beh跟tuè%{filename} 內底ê %{count} ê口座,而且停止跟tuè別lâng。 + lists_html: + other: Lí當beh用 %{filename} ê內容取代lí ê列單%{count} ê口座會加添kàu新列單。 + muting_html: + other: Lí teh-beh用 %{filename} ê %{count} ê口座替換lí ê消音口座ê列單。 + preambles: + blocking_html: + other: Lí teh-beh kā %{filename}內底ê%{count} ê口座封鎖。 + bookmarks_html: + other: Lí當beh對 %{filename} 加添 %{count} 篇PO文kàu lí ê 冊籤。 + domain_blocking_html: + other: Lí teh-beh kā %{filename}內底ê%{count} ê域名封鎖。 + following_html: + other: Lí teh-beh kā %{filename}內底ê%{count} ê口座跟tuè。 + lists_html: + other: Lí當beh對 %{filename}%{count} ê口座 kàu lí ê列單。若無列單通加添,新ê列單ē建立。 + muting_html: + other: Lí teh-beh kā %{filename}內底ê%{count} ê口座消音。 + preface: Lí ē當輸入lí對別ê服侍器輸出ê資料,比如lí所跟tuè ê á是封鎖ê ê列單。 + recent_imports: 最近輸入ê + login_activities: + authentication_methods: + webauthn: 安全檢查 scheduled_statuses: too_soon: Tio̍h用未來ê日期。 statuses: diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 72c8ece6f2bbb8..bacad16d67abdf 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -7,6 +7,8 @@ nl: hosted_on: Mastodon op %{domain} title: Over accounts: + errors: + cannot_be_added_to_collections: Dit account kan niet aan collecties worden toegevoegd. followers: one: Volger other: Volgers @@ -2186,4 +2188,5 @@ nl: otp_required: Om beveiligingssleutels te kunnen gebruiken, moet je eerst tweestapsverificatie inschakelen. registered_on: Geregistreerd op %{date} wrapstodon: + description: Bekijk hoe %{name} dit jaar Mastodon heeft gebruikt! title: Wrapstodon %{year} voor %{name} diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index 657ab9f6ad6da2..7e3344efff4a86 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -7,6 +7,8 @@ pt-BR: hosted_on: Mastodon hospedado em %{domain} title: Sobre accounts: + errors: + cannot_be_added_to_collections: Esta conta não pode ser adicionada a coleções. followers: one: Seguidor other: Seguidores @@ -2187,4 +2189,5 @@ pt-BR: otp_required: Para usar chaves de segurança, ative a autenticação de dois fatores. registered_on: Registrado em %{date} wrapstodon: + description: Veja como %{name} usou o Mastodon este ano! title: Wrapstodon de %{year} para %{name} diff --git a/config/locales/pt-PT.yml b/config/locales/pt-PT.yml index 5aca8c4ec4cfee..b9175395f0ccda 100644 --- a/config/locales/pt-PT.yml +++ b/config/locales/pt-PT.yml @@ -7,6 +7,8 @@ pt-PT: hosted_on: Mastodon alojado em %{domain} title: Sobre accounts: + errors: + cannot_be_added_to_collections: Esta conta não pode ser adicionada às coleções. followers: one: Seguidor other: Seguidores @@ -846,6 +848,7 @@ pt-PT: publish_statistics: Publicar estatísticas title: Descobrir trends: Tendências + wrapstodon: Wrapstodon domain_blocks: all: Para toda a gente disabled: Para ninguém @@ -1715,7 +1718,7 @@ pt-PT: subject: "%{name} citou a sua publicação" title: Nova citação quoted_update: - subject: "%{name} editou uma publicação que citaste" + subject: "%{name} editou uma publicação que citou" reblog: body: 'A tua publicação foi partilhada por %{name}:' subject: "%{name} partilhou a sua publicação" @@ -2185,3 +2188,6 @@ pt-PT: not_supported: Este navegador não funciona com chaves de segurança otp_required: Para utilizares chaves de segurança, ativa primeiro a autenticação de dois fatores. registered_on: Registado em %{date} + wrapstodon: + description: Veja como %{name} utilizou o Mastodon este ano! + title: Wrapstodon %{year} de %{name} diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 4e61fd52b392f8..7441211dd11672 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -521,10 +521,12 @@ ru: registrations: confirm: Подтвердить reject: Отклонить + title: Подтвердить регистрацию FASP save: Сохранить select_capabilities: Выберите возможности sign_in: status: Пост + title: Fediverse Auxiliary Service Providers title: FASP follow_recommendations: description_html: "Рекомендации профилей помогают новым пользователям быстрее найти что-нибудь интересное. Если пользователь мало взаимодействовал с другими и составить персонализированные рекомендации не получается, будут предложены указанные здесь профили. Эти рекомендации обновляются ежедневно из совокупности учётных записей с наибольшим количеством недавних взаимодействий и наибольшим количеством подписчиков с этого сервера для данного языка." diff --git a/config/locales/simple_form.be.yml b/config/locales/simple_form.be.yml index dbc556bf85efa3..7f899b5139b481 100644 --- a/config/locales/simple_form.be.yml +++ b/config/locales/simple_form.be.yml @@ -111,6 +111,7 @@ be: thumbnail: Выява памерамі прыкладна 2:1, якая паказваецца побач з інфармацыяй пра ваш сервер. trendable_by_default: Прапусціць ручны агляд трэндавага змесціва. Асобныя элементы ўсё яшчэ можна будзе выдаліць з трэндаў пастфактум. trends: Трэнды паказваюць, якія допісы, хэштэгі і навіны набываюць папулярнасць на вашым серверы. + wrapstodon: Прапанаваць мясцовым карыстальнікам інтэрактыўную зводку іх выкарыстання Mastodon на працягу года. Гэта функцыя даступная паміж 10-ым і 31-ым снежня кожнага года і прапаноўваецца карыстальнікам, якія зрабілі хаця б адзін публічны або ціхі публічны допіс, а таксама выкарысталі хаця б адзін хэштэг на працягу года. form_challenge: current_password: Вы ўваходзіце ў бяспечную зону imports: @@ -314,6 +315,7 @@ be: thumbnail: Мініяцюра сервера trendable_by_default: Дазваляць трэнды без папярэдняй праверкі trends: Уключыць трэнды + wrapstodon: Уключыць Вынікадон interactions: must_be_follower: Заблакіраваць апавяшчэнні ад непадпісаных людзей must_be_following: Заблакіраваць апавяшчэнні ад людзей на якіх вы не падпісаны diff --git a/config/locales/simple_form.da.yml b/config/locales/simple_form.da.yml index 68f114e7fe2bd3..4df4e34eedee1f 100644 --- a/config/locales/simple_form.da.yml +++ b/config/locales/simple_form.da.yml @@ -111,6 +111,7 @@ da: thumbnail: Et ca. 2:1 billede vist sammen med serveroplysningerne. trendable_by_default: Spring manuel gennemgang af trendindhold over. Individuelle elementer kan stadig fjernes fra trends efter kendsgerningen. trends: Tendenser viser, hvilke indlæg, hashtags og nyheder opnår momentum på serveren. + wrapstodon: Tilbyd lokale brugere at generere en sjov oversigt over deres brug af Mastodon i løbet af året. Denne funktion er tilgængelig mellem den 10. og 31. december hvert år og tilbydes til brugere, der har lavet mindst ét offentligt eller stille offentligt indlæg og brugt mindst ét hashtag i løbet af året. form_challenge: current_password: Du bevæger dig ind på et sikkert område imports: @@ -312,6 +313,7 @@ da: thumbnail: Serverminiaturebillede trendable_by_default: Tillad ikke-reviderede trends trends: Aktivér trends + wrapstodon: Aktivér Wrapstodon interactions: must_be_follower: Blokér notifikationer fra bruger, der ikke følger dig must_be_following: Blokér notifikationer fra brugere, du ikke følger diff --git a/config/locales/simple_form.de.yml b/config/locales/simple_form.de.yml index e085ccd2eedb9f..a8755d69010e37 100644 --- a/config/locales/simple_form.de.yml +++ b/config/locales/simple_form.de.yml @@ -111,6 +111,7 @@ de: thumbnail: Ein Bild ungefähr im 2:1-Format, das neben den Server-Informationen angezeigt wird. trendable_by_default: Manuelles Überprüfen angesagter Inhalte überspringen. Einzelne Elemente können später noch aus den Trends entfernt werden. trends: Trends zeigen, welche Beiträge, Hashtags und Nachrichten auf deinem Server immer beliebter werden. + wrapstodon: Ermöglicht Nutzer*innen dieses Servers einen spielerischen Jahresrückblick ihrer Mastodon-Aktivität zu erstellen. Diese Funktion ist jedes Jahr zwischen dem 10. und 31. Dezember verfügbar und wird Nutzer*innen angeboten, die innerhalb des Jahres mindestens einen öffentlichen oder stillen Beitrag verfasst und mindestens einen Hashtag verwendet haben. form_challenge: current_password: Du betrittst einen sicheren Bereich imports: @@ -312,6 +313,7 @@ de: thumbnail: Vorschaubild des Servers trendable_by_default: Trends ohne vorherige Überprüfung erlauben trends: Trends aktivieren + wrapstodon: Wrapstodon aktivieren interactions: must_be_follower: Benachrichtigungen von Profilen, die mir nicht folgen, ausblenden must_be_following: Benachrichtigungen von Profilen, denen ich nicht folge, ausblenden diff --git a/config/locales/simple_form.el.yml b/config/locales/simple_form.el.yml index f033d90dcfaae1..a579cafbceb9bc 100644 --- a/config/locales/simple_form.el.yml +++ b/config/locales/simple_form.el.yml @@ -49,7 +49,7 @@ el: email: Θα σου σταλεί email επιβεβαίωσης header: WEBP, PNG, GIF ή JPG. Το πολύ %{size}. Θα υποβαθμιστεί σε %{dimensions}px inbox_url: Αντέγραψε το URL της αρχικής σελίδας του ανταποκριτή που θέλεις να χρησιμοποιήσεις - irreversible: Οι φιλτραρισμένες αναρτήσεις θα εξαφανιστούν αμετάκλητα, ακόμα και αν το φίλτρο αργότερα αφαιρεθεί + irreversible: Οι φιλτραρισμένες αναρτήσεις θα εξαφανιστούν αμετάκλητα, ακόμη και αν το φίλτρο αργότερα αφαιρεθεί locale: Η γλώσσα χρήσης, των email και των ειδοποιήσεων push password: Χρησιμοποίησε τουλάχιστον 8 χαρακτήρες phrase: Θα ταιριάζει ανεξαρτήτως πεζών/κεφαλαίων ή προειδοποίησης περιεχομένου μιας ανάρτησης @@ -95,7 +95,7 @@ el: favicon: WEBP, PNG, GIF ή JPG. Παρακάμπτει το προεπιλεγμένο favicon του Mastodon με ένα προσαρμοσμένο εικονίδιο. landing_page: Επιλέγει ποια σελίδα βλέπουν οι νέοι επισκέπτες όταν φτάνουν για πρώτη φορά στο διακομιστή σας. Αν επιλέξετε "Τάσεις", τότε οι τάσεις πρέπει να είναι ενεργοποιημένες στις Ρυθμίσεις Ανακάλυψης. Αν επιλέξετε "Τοπική ροή", τότε το "Πρόσβαση σε ζωντανές ροές με τοπικές αναρτήσεις" πρέπει να οριστεί σε "Όλοι" στις Ρυθμίσεις Ανακάλυψης. mascot: Παρακάμπτει την εικονογραφία στην προηγμένη διεπαφή ιστού. - media_cache_retention_period: Τα αρχεία πολυμέσων από αναρτήσεις που γίνονται από απομακρυσμένους χρήστες αποθηκεύονται προσωρινά στο διακομιστή σου. Όταν οριστεί μια θετική τιμή, τα μέσα θα διαγραφούν μετά τον καθορισμένο αριθμό ημερών. Αν τα δεδομένα πολυμέσων ζητηθούν μετά τη διαγραφή τους, θα γίνει ε, αν το πηγαίο περιεχόμενο είναι ακόμα διαθέσιμο. Λόγω περιορισμών σχετικά με το πόσο συχνά οι κάρτες προεπισκόπησης συνδέσμων συνδέονται σε ιστοσελίδες τρίτων, συνιστάται να ορίσεις αυτή την τιμή σε τουλάχιστον 14 ημέρες ή οι κάρτες προεπισκόπησης συνδέσμων δεν θα ενημερώνονται κατ' απάιτηση πριν από εκείνη την ώρα. + media_cache_retention_period: Τα αρχεία πολυμέσων από αναρτήσεις που γίνονται από απομακρυσμένους χρήστες αποθηκεύονται προσωρινά στο διακομιστή σου. Όταν οριστεί μια θετική τιμή, τα μέσα θα διαγραφούν μετά τον καθορισμένο αριθμό ημερών. Αν τα δεδομένα πολυμέσων ζητηθούν μετά τη διαγραφή τους, θα γίνει λήψη τους ξανά, αν το πηγαίο περιεχόμενο είναι ακόμη διαθέσιμο. Λόγω περιορισμών σχετικά με το πόσο συχνά οι κάρτες προεπισκόπησης συνδέσμων συνδέονται σε ιστοσελίδες τρίτων, συνιστάται να ορίσεις αυτή την τιμή σε τουλάχιστον 14 ημέρες ή οι κάρτες προεπισκόπησης συνδέσμων δεν θα ενημερώνονται κατ' απάιτηση πριν από εκείνη την ώρα. min_age: Οι χρήστες θα κληθούν να επιβεβαιώσουν την ημερομηνία γέννησής τους κατά την εγγραφή peers_api_enabled: Μια λίστα με ονόματα τομέα που συνάντησε αυτός ο διακομιστής στο fediverse. Δεν περιλαμβάνονται δεδομένα εδώ για το αν συναλλάσσετε με ένα συγκεκριμένο διακομιστή, μόνο ότι ο διακομιστής σας το ξέρει. Χρησιμοποιείται από υπηρεσίες που συλλέγουν στατιστικά στοιχεία για την συναλλαγή με γενική έννοια. profile_directory: Ο κατάλογος προφίλ παραθέτει όλους τους χρήστες που έχουν επιλέξει να είναι ανακαλύψιμοι. @@ -109,8 +109,9 @@ el: status_page_url: Το URL μιας σελίδας όπου κάποιος μπορεί να δει την κατάσταση αυτού του διακομιστή κατά τη διάρκεια μιας διακοπής λειτουργίας theme: Θέμα που βλέπουν αποσυνδεδεμένοι επισκέπτες ή νέοι χρήστες. thumbnail: Μια εικόνα περίπου 2:1 που εμφανίζεται παράλληλα με τις πληροφορίες του διακομιστή σου. - trendable_by_default: Παράλειψη χειροκίνητης αξιολόγησης του περιεχομένου σε τάση. Μεμονωμένα στοιχεία μπορούν ακόμα να αφαιρεθούν από τις τάσεις μετέπειτα. + trendable_by_default: Παράλειψη χειροκίνητης αξιολόγησης του περιεχομένου σε τάση. Μεμονωμένα στοιχεία μπορούν ακόμη να αφαιρεθούν από τις τάσεις μετέπειτα. trends: Τάσεις δείχνουν ποιες δημοσιεύσεις, ετικέτες και ειδήσεις προκαλούν έλξη στο διακομιστή σας. + wrapstodon: Πρόσφερε στους τοπικούς χρήστες τη δυνατότητα να δημιουργήσουν μια παιχνιδιάρικη σύνοψη της χρήσης τους Mastodon κατά τη διάρκεια του έτους. Αυτό το χαρακτηριστικό είναι διαθέσιμο μεταξύ της 10ης και της 31ης Δεκεμβρίου κάθε έτους, και προσφέρεται σε χρήστες που έκαναν τουλάχιστον μία δημόσια ή ήσυχη δημόσια θέση και χρησιμοποίησαν τουλάχιστον μία ετικέτα εντός του έτους. form_challenge: current_password: Μπαίνεις σε ασφαλή περιοχή imports: @@ -312,6 +313,7 @@ el: thumbnail: Μικρογραφία διακομιστή trendable_by_default: Επίτρεψε τις τάσεις χωρίς προηγούμενη αξιολόγηση trends: Ενεργοποίηση τάσεων + wrapstodon: Ενεργοποίηση Wrapstodon interactions: must_be_follower: Μπλόκαρε τις ειδοποιήσεις από όσους δεν σε ακολουθούν must_be_following: Μπλόκαρε τις ειδοποιήσεις από όσους δεν ακολουθείς diff --git a/config/locales/simple_form.en-GB.yml b/config/locales/simple_form.en-GB.yml index a749302353821d..db4080c11bbfb2 100644 --- a/config/locales/simple_form.en-GB.yml +++ b/config/locales/simple_form.en-GB.yml @@ -10,13 +10,13 @@ en-GB: indexable: Your public posts may appear in search results on Mastodon. People who have interacted with your posts may be able to search them regardless. note: 'You can @mention other people or #hashtags.' show_collections: People will be able to browse through your follows and followers. People that you follow will see that you follow them regardless. - unlocked: People will be able to follow you without requesting approval. Uncheck if you want to review follow requests and choose whether to accept or reject new followers. + unlocked: People will be able to follow you without requesting approval. Untick if you want to review follow requests and choose whether to accept or reject new followers. account_alias: acct: Specify the username@domain of the account you want to move from account_migration: acct: Specify the username@domain of the account you want to move to account_warning_preset: - text: You can use post syntax, such as URLs, hashtags and mentions + text: You can use post syntax, such as URLs, hashtags, and mentions title: Optional. Not visible to the recipient admin_account_action: include_statuses: The user will see which posts have caused the moderation action or warning @@ -29,9 +29,9 @@ en-GB: sensitive: Force all this user's media attachments to be flagged as sensitive. silence: Prevent the user from being able to post with public visibility, hide their posts and notifications from people not following them. Closes all reports against this account. suspend: Prevent any interaction from or to this account and delete its contents. Revertible within 30 days. Closes all reports against this account. - warning_preset_id: Optional. You can still add custom text to end of the preset + warning_preset_id: Optional. You can still add custom text to the end of the preset announcement: - all_day: When checked, only the dates of the time range will be displayed + all_day: When ticked, only the dates of the time range will be displayed ends_at: Optional. Announcement will be automatically unpublished at this time scheduled_at: Leave blank to publish the announcement immediately starts_at: Optional. In case your announcement is bound to a specific time range @@ -43,20 +43,20 @@ en-GB: avatar: WEBP, PNG, GIF or JPG. At most %{size}. Will be downscaled to %{dimensions}px bot: Signal to others that the account mainly performs automated actions and might not be monitored context: One or multiple contexts where the filter should apply - current_password: For security purposes please enter the password of the current account + current_password: For security purposes, please enter the password of the current account current_username: To confirm, please enter the username of the current account digest: Only sent after a long period of inactivity and only if you have received any personal messages in your absence - email: You will be sent a confirmation e-mail + email: You will be sent a confirmation email header: WEBP, PNG, GIF or JPG. At most %{size}. Will be downscaled to %{dimensions}px - inbox_url: Copy the URL from the frontpage of the relay you want to use - irreversible: Filtered posts will disappear irreversibly, even if filter is later removed - locale: The language of the user interface, e-mails and push notifications - password: Use at least 8 characters + inbox_url: Copy the URL from the front page of the relay you want to use + irreversible: Filtered posts will disappear irreversibly, even if the filter is later removed + locale: The language of the user interface, emails, and push notifications + password: Use at least eight characters phrase: Will be matched regardless of casing in text or content warning of a post scopes: Which APIs the application will be allowed to access. If you select a top-level scope, you don't need to select individual ones. setting_advanced_layout: Display Mastodon as a multi-column layout, allowing you to view the timeline, notifications, and a third column of your choosing. Not recommended for smaller screens. setting_aggregate_reblogs: Do not show new boosts for posts that have been recently boosted (only affects newly-received boosts) - setting_always_send_emails: Normally e-mail notifications won't be sent when you are actively using Mastodon + setting_always_send_emails: Normally email notifications won't be sent when you are actively using Mastodon setting_boost_modal: When enabled, boosting will first open a confirmation dialogue in which you can change the visibility of your boost. setting_default_quote_policy_private: Followers-only posts authored on Mastodon can't be quoted by others. setting_default_quote_policy_unlisted: When people quote you, their post will also be hidden from trending timelines. @@ -67,14 +67,14 @@ en-GB: setting_emoji_style: How to display emojis. "Auto" will try using native emoji, but falls back to Twemoji for legacy browsers. setting_quick_boosting_html: When enabled, clicking on the %{boost_icon} Boost icon will immediately boost instead of opening the boost/quote dropdown menu. Relocates the quoting action to the %{options_icon} (Options) menu. setting_system_scrollbars_ui: Applies only to desktop browsers based on Safari and Chrome - setting_use_blurhash: Gradients are based on the colors of the hidden visuals but obfuscate any details + setting_use_blurhash: Gradients are based on the colours of the hidden visuals but obfuscate any details setting_use_pending_items: Hide timeline updates behind a click instead of automatically scrolling the feed username: You can use letters, numbers, and underscores whole_word: When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word domain_allow: domain: This domain will be able to fetch data from this server and incoming data from it will be processed and stored email_domain_block: - domain: This can be the domain name that shows up in the e-mail address or the MX record it uses. They will be checked upon sign-up. + domain: This can be the domain name that shows up in the email address or the MX record it uses. They will be checked upon sign-up. with_dns_records: An attempt to resolve the given domain's DNS records will be made and the results will also be blocked featured_tag: name: 'Here are some of the hashtags you used the most recently:' @@ -90,14 +90,14 @@ en-GB: backups_retention_period: Users have the ability to generate archives of their posts to download later. When set to a positive value, these archives will be automatically deleted from your storage after the specified number of days. bootstrap_timeline_accounts: These accounts will be pinned to the top of new users' follow recommendations. Provide a comma-separated list of accounts. closed_registrations_message: Displayed when sign-ups are closed - content_cache_retention_period: All posts from other servers (including boosts and replies) will be deleted after the specified number of days, without regard to any local user interaction with those posts. This includes posts where a local user has marked it as bookmarks or favorites. Private mentions between users from different instances will also be lost and impossible to restore. Use of this setting is intended for special purpose instances and breaks many user expectations when implemented for general purpose use. + content_cache_retention_period: All posts from other servers (including boosts and replies) will be deleted after the specified number of days, without regard to any local user interaction with those posts. This includes posts where a local user has marked it as bookmarks or favourites. Private mentions between users from different instances will also be lost and impossible to restore. Use of this setting is intended for special purpose instances and breaks many user expectations when implemented for general purpose use. custom_css: You can apply custom styles on the web version of Mastodon. favicon: WEBP, PNG, GIF or JPG. Overrides the default Mastodon favicon with a custom icon. landing_page: Selects what page new visitors see when they first arrive on your server. If you select "Trends", then Trends needs to be enabled in the Discovery Settings. If you select "Local feed", then "Access to live feeds featuring local posts" needs to be set to "Everyone" in the Discovery Settings. mascot: Overrides the illustration in the advanced web interface. media_cache_retention_period: Media files from posts made by remote users are cached on your server. When set to a positive value, media will be deleted after the specified number of days. If the media data is requested after it is deleted, it will be re-downloaded, if the source content is still available. Due to restrictions on how often link preview cards poll third-party sites, it is recommended to set this value to at least 14 days, or link preview cards will not be updated on demand before that time. min_age: Users will be asked to confirm their date of birth during sign-up - peers_api_enabled: A list of domain names this server has encountered in the fediverse. No data is included here about whether you federate with a given server, just that your server knows about it. This is used by services that collect statistics on federation in a general sense. + peers_api_enabled: A list of domain names this server has encountered in the Fediverse. No data is included here about whether you federate with a given server, just that your server knows about it. This is used by services that collect statistics on federation in a general sense. profile_directory: The profile directory lists all users who have opted-in to be discoverable. require_invite_text: When sign-ups require manual approval, make the “Why do you want to join?” text input mandatory rather than optional site_contact_email: How people can reach you for legal or support inquiries. @@ -110,7 +110,8 @@ en-GB: theme: Theme that logged out visitors and new users see. thumbnail: A roughly 2:1 image displayed alongside your server information. trendable_by_default: Skip manual review of trending content. Individual items can still be removed from trends after the fact. - trends: Trends show which posts, hashtags and news stories are gaining traction on your server. + trends: Trends show which posts, hashtags, and news stories are gaining traction on your server. + wrapstodon: Offer local users to generate a playful summary of their Mastodon use during the year. This feature is available between the 10th and 31st of December of each year, and is offered to users who made at least one Public or Quiet Public post and used at least one hashtag within the year. form_challenge: current_password: You are entering a secure area imports: @@ -131,7 +132,7 @@ en-GB: text: Describe a rule or requirement for users on this server. Try to keep it short and simple sessions: otp: 'Enter the two-factor code generated by your phone app or use one of your recovery codes:' - webauthn: If it's an USB key be sure to insert it and, if necessary, tap it. + webauthn: If it's a USB key be sure to insert it and, if necessary, tap it. settings: indexable: Your profile page may appear in search results on Google, Bing, and others. show_application: You will always be able to see which app published your post regardless. @@ -145,11 +146,11 @@ en-GB: admin_email: Legal notices include counternotices, court orders, takedown requests, and law enforcement requests. arbitration_address: Can be the same as Physical address above, or “N/A” if using email. arbitration_website: Can be a web form, or “N/A” if using email. - choice_of_law: City, region, territory or state the internal substantive laws of which shall govern any and all claims. + choice_of_law: City, region, territory, or state, the internal substantive laws of which shall govern any and all claims. dmca_address: For US operators, use the address registered in the DMCA Designated Agent Directory. A P.O. Box listing is available upon direct request, use the DMCA Designated Agent Post Office Box Waiver Request to email the Copyright Office and describe that you are a home-based content moderator who fears revenge or retribution for your actions and need to use a P.O. Box to remove your home address from public view. dmca_email: Can be the same email used for “Email address for legal notices” above. domain: Unique identification of the online service you are providing. - jurisdiction: List the country where whoever pays the bills lives. If it’s a company or other entity, list the country where it’s incorporated, and the city, region, territory or state as appropriate. + jurisdiction: List the country where whoever pays the bills lives. If it’s a company or other entity, list the country where it’s incorporated, and the city, region, territory, or state as appropriate. min_age: Should not be below the minimum age required by the laws of your jurisdiction. user: chosen_languages: When checked, only posts in selected languages will be displayed in public timelines @@ -158,7 +159,7 @@ en-GB: other: We have to make sure you're at least %{count} to use %{domain}. We won't store this. role: The role controls which permissions the user has. user_role: - color: Color to be used for the role throughout the UI, as RGB in hex format + color: Colour to be used for the role throughout the UI, as RGB in hex format highlighted: This makes the role publicly visible name: Public name of the role, if role is set to be displayed as a badge permissions_as_keys: Users with this role will have access to... @@ -170,7 +171,7 @@ en-GB: webhook: events: Select events to send template: Compose your own JSON payload using variable interpolation. Leave blank for default JSON. - url: Where events will be sent to + url: Where events will be sent labels: account: attribution_domains: Websites allowed to credit you @@ -189,8 +190,8 @@ en-GB: text: Preset text title: Title admin_account_action: - include_statuses: Include reported posts in the e-mail - send_email_notification: Notify the user per e-mail + include_statuses: Include reported posts in the email + send_email_notification: Notify the user per email text: Custom warning type: Action types: @@ -219,7 +220,7 @@ en-GB: current_password: Current password data: Data display_name: Display name - email: E-mail address + email: Email address expires_in: Expire after fields: Profile metadata header: Header @@ -227,7 +228,7 @@ en-GB: inbox_url: URL of the relay inbox irreversible: Drop instead of hide locale: Interface language - max_uses: Max number of uses + max_uses: Maximum number of uses new_password: New password note: Bio otp_attempt: Two-factor code @@ -235,7 +236,7 @@ en-GB: phrase: Keyword or phrase setting_advanced_layout: Enable advanced web interface setting_aggregate_reblogs: Group boosts in timelines - setting_always_send_emails: Always send e-mail notifications + setting_always_send_emails: Always send email notifications setting_auto_play_gif: Auto-play animated GIFs setting_boost_modal: Control boosting visibility setting_default_language: Posting language @@ -259,7 +260,7 @@ en-GB: setting_system_scrollbars_ui: Use system's default scrollbar setting_theme: Site theme setting_trends: Show today's trends - setting_unfollow_modal: Show confirmation dialog before unfollowing someone + setting_unfollow_modal: Show confirmation dialogue before unfollowing someone setting_use_blurhash: Show colourful gradients for hidden media setting_use_pending_items: Slow mode severity: Severity @@ -295,13 +296,13 @@ en-GB: min_age: Minimum age requirement peers_api_enabled: Publish list of discovered servers in the API profile_directory: Enable profile directory - registrations_mode: Who can sign-up + registrations_mode: Who can sign up remote_live_feed_access: Access to live feeds featuring remote posts remote_topic_feed_access: Access to hashtag and link feeds featuring remote posts require_invite_text: Require a reason to join show_domain_blocks: Show domain blocks show_domain_blocks_rationale: Show why domains were blocked - site_contact_email: Contact e-mail + site_contact_email: Contact email site_contact_username: Contact username site_extended_description: Extended description site_short_description: Server description @@ -312,6 +313,7 @@ en-GB: thumbnail: Server thumbnail trendable_by_default: Allow trends without prior review trends: Enable trends + wrapstodon: Enable Wrapstodon interactions: must_be_follower: Block notifications from non-followers must_be_following: Block notifications from people you don't follow @@ -344,7 +346,7 @@ en-GB: critical: Notify on critical updates only label: A new Mastodon version is available none: Never notify of updates (not recommended) - patch: Notify on bugfix updates + patch: Notify on bug fix updates trending_tag: New trend requires review rule: hint: Additional information diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 288ef8cf580e36..f8fab44037f449 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -146,6 +146,7 @@ en: thumbnail: A roughly 2:1 image displayed alongside your server information. trendable_by_default: Skip manual review of trending content. Individual items can still be removed from trends after the fact. trends: Trends show which posts, hashtags and news stories are gaining traction on your server. + wrapstodon: Offer local users to generate a playful summary of their Mastodon use during the year. This feature is available between the 10th and 31st of December of each year, and is offered to users who made at least one Public or Quiet Public post and used at least one hashtag within the year. form_challenge: current_password: You are entering a secure area imports: @@ -287,6 +288,7 @@ en: setting_bio_markdown: Enable profile markdown setting_bookmark_category_needed: Category selection needed when registering bookmark on web setting_boost_modal: Control boosting visibility + setting_color_scheme: Mode setting_community_timeline_instead_of_search_menu: Show local timeline on phone navigation instead of search setting_content_font_size: Font size of posts setting_content_font_size_items: @@ -294,6 +296,7 @@ en: medium: Default x_large: Large large xx_large: Large large large + setting_contrast: Contrast setting_custom_css: Custom CSS setting_default_language: Posting language setting_default_privacy: Posting visibility @@ -449,6 +452,7 @@ en: trendable_by_default: Allow trends without prior review trends: Enable trends unlocked_friend: Accept all friend server follows automatically + wrapstodon: Enable Wrapstodon interactions: must_be_follower: Block notifications from non-followers must_be_following: Block notifications from people you don't follow diff --git a/config/locales/simple_form.es-AR.yml b/config/locales/simple_form.es-AR.yml index 4689dd0d79b09b..971ff038c48dc9 100644 --- a/config/locales/simple_form.es-AR.yml +++ b/config/locales/simple_form.es-AR.yml @@ -111,6 +111,7 @@ es-AR: thumbnail: Una imagen de aproximadamente 2:1 se muestra junto a la información de tu servidor. trendable_by_default: Omití la revisión manual del contenido en tendencia. Los elementos individuales aún podrán eliminarse de las tendencias. trends: Las tendencias muestran qué mensajes, etiquetas y noticias están ganando tracción en tu servidor. + wrapstodon: Ofrecer a los usuarios locales un resumen lúdico de su uso en Mastodon durante el año. Esta función está disponible entre los días 10 y 31 de diciembre de cada año, y se ofrece a los usuarios que enviaron a Mastodon, al menos, un mensaje público o un mensaje público pero silencioso, y que utilizaron, al menos, una etiqueta durante el año. form_challenge: current_password: Estás ingresando en un área segura imports: @@ -312,6 +313,7 @@ es-AR: thumbnail: Miniatura del servidor trendable_by_default: Permitir tendencias sin revisión previa trends: Habilitar tendencias + wrapstodon: Habilitar MastodonAnual interactions: must_be_follower: Bloquear notificaciones de cuentas que no te siguen must_be_following: Bloquear notificaciones de cuentas que no seguís diff --git a/config/locales/simple_form.es-MX.yml b/config/locales/simple_form.es-MX.yml index ae46605da65200..cfcbc77f3c4938 100644 --- a/config/locales/simple_form.es-MX.yml +++ b/config/locales/simple_form.es-MX.yml @@ -111,6 +111,7 @@ es-MX: thumbnail: Una imagen de aproximadamente 2:1 se muestra junto a la información de tu servidor. trendable_by_default: Omitir la revisión manual del contenido en tendencia. Los elementos individuales aún podrán eliminarse de las tendencias. trends: Las tendencias muestran qué mensajes, etiquetas y noticias están ganando tracción en tu servidor. + wrapstodon: Ofrece a los usuarios locales la posibilidad de generar un resumen divertido de su uso de Mastodon durante el año. Esta función está disponible entre el 10 y el 31 de diciembre de cada año, y se ofrece a los usuarios que hayan publicado al menos una publicación pública o pública silenciosa y hayan utilizado al menos una etiqueta durante el año. form_challenge: current_password: Estás entrando en un área segura imports: @@ -312,6 +313,7 @@ es-MX: thumbnail: Miniatura del servidor trendable_by_default: Permitir tendencias sin revisión previa trends: Habilitar tendencias + wrapstodon: Habilitar Wrapstodon interactions: must_be_follower: Bloquear notificaciones de personas que no te siguen must_be_following: Bloquear notificaciones de personas que no sigues diff --git a/config/locales/simple_form.eu.yml b/config/locales/simple_form.eu.yml index 26888fd24f2693..0e36f83c341dc3 100644 --- a/config/locales/simple_form.eu.yml +++ b/config/locales/simple_form.eu.yml @@ -54,8 +54,12 @@ eu: password: Erabili 8 karaktere gutxienez phrase: Bat egingo du Maiuskula/minuskula kontuan hartu gabe eta edukiaren abisua kontuan hartu gabe scopes: Zeintzuk API atzitu ditzakeen aplikazioak. Goi mailako arloa aukeratzen baduzu, ez dituzu azpikoak aukeratu behar. + setting_advanced_layout: Erakutsi Mastodon hainbat zutaberen ikuspegiarekin, zure kronologia, jakinarazpenak eta zuk aukeratutako hirugarren zutabe bat ikusteko aukera emanez. Ez da gomendagarria pantaila txikietarako. setting_aggregate_reblogs: Ez erakutsi bultzada berriak berriki bultzada jaso duten tootentzat (berriki jasotako bultzadei eragiten die bakarrik) setting_always_send_emails: Normalean eposta jakinarazpenak ez dira bidaliko Mastodon aktiboki erabiltzen ari zaren bitartean + setting_boost_modal: Gaituta dagoenean, bultzada emateak berrespen-leiho bat irekiko du lehenik; bertan, bultzadaren ikusgaitasuna aldatu ahal izango duzu. + setting_default_quote_policy_private: Jarraitzaileentzat soilik sortutako bidalketak Mastodonen ezin dituzte beste batzuek aipatu. + setting_default_quote_policy_unlisted: Jendeak aipatzen zaituenean, bere bidalketa ere joeren denbora-lerro publikoetatik ezkutatuko da. setting_default_sensitive: Multimedia hunkigarria lehenetsita ezkutatzen da, eta sakatuz ikusi daiteke setting_display_media_default: Ezkutatu hunkigarri gisa markatutako multimedia setting_display_media_hide_all: Ezkutatu multimedia guztia beti @@ -74,6 +78,7 @@ eu: featured_tag: name: 'Hemen dituzu azkenaldian gehien erabili dituzun traoletako batzuk:' filters: + action: Aukeratu ze ekintza burutu behar den bidalketa bat iragazkiarekin bat datorrenean actions: blur: Ezkutatu edukia ohar baten atzean, testua bera ezkutatu gabe hide: Ezkutatu erabat iragazitako edukia, existituko ez balitz bezala @@ -82,6 +87,7 @@ eu: activity_api_enabled: Lokalki argitaratutako bidalketak, erabiltzaile aktiboak, eta izen-emateen kopuruak astero zenbatzen ditu app_icon: WEBP, PNG, GIF edo JPG. Aplikazioaren ikono lehenetsia gainidazten du ikono pertsonalizatu batekin gailu mugikorretan. backups_retention_period: Erabiltzaileek geroago deskarga dezaketen beren argitalpenen artxiboak sor ditzakete. Balio positibo bat ezartzean, artxibo hauek biltegiratzetik automatikoki ezabatuko dira zehazturiko egunen buruan. + bootstrap_timeline_accounts: Erabiltzaile berrien jarraipenerako gomendioen goiko aldean agertuko dira kontu horiek. Eman komaz bereizitako kontuen zerrenda. closed_registrations_message: Izen-ematea itxia dagoenean bistaratua content_cache_retention_period: Beste zerbitzarietako argitalpen guztiak (bultzadak eta erantzunak barne) ezabatuko dira zehazturiko egunen buruan, argitalpen horiek izan ditzaketen erabiltzaile lokalaren interakzioa kontuan izanik gabe. Instantzia desberdinetako erabiltzaileen arteko aipamen pribatuak ere galdu egingo dira eta ezin izango dira berreskuratu. Ezarpen honen erabilera xede berezia duten instantziei zuzendua dago eta erabiltzaileen itxaropena hausten da orotariko erabilerarako inplementatzean. custom_css: Estilo pertsonalizatuak aplikatu ditzakezu Mastodonen web bertsioan. @@ -103,6 +109,7 @@ eu: thumbnail: Zerbitzariaren informazioaren ondoan erakusten den 2:1 inguruko irudia. trendable_by_default: Saltatu joeretako edukiaren eskuzko berrikuspena. Ondoren elementuak banan-bana kendu daitezke joeretatik. trends: Joeretan zure zerbitzarian bogan dauden bidalketa, traola eta albisteak erakusten dira. + wrapstodon: Bertako erabiltzaileei Mastodonen urteko erabileraren laburpen ludiko bat sortzea eskaini. Ezaugarri hori urte bakoitzeko abenduaren 10etik 31ra bitartean dago eskuragarri, eta urtean gutxienez hashtag bat erabili duten erabiltzaileei eskaintzen zaie. form_challenge: current_password: Zonalde seguruan sartzen ari zara imports: @@ -137,14 +144,23 @@ eu: admin_email: Legezko abisuak, kontraindikazioak, agindu judizialak, erretiratzeko eskaerak eta legea betetzeko eskaerak barne. arbitration_address: Goiko helbide fisikoa edo "N/A" bera izan daiteke posta elektronikoa erabiliz gero. arbitration_website: Web formularioa izan daiteke, edo "N/A" posta elektronikoa erabiliz gero. + choice_of_law: Edozein gatazka juridiko gobernatuko duten epaitegien hiria, eskualdea, lurraldea edo estatua. + dmca_email: Goiko "Lege-oharretarako helbide elektronikoa" atalean erabilitako helbide elektroniko bera izan daiteke. + domain: Ematen ari zaren lineako zerbitzuaren identifikazio bakarra. + jurisdiction: Zerrendatu fakturak ordaintzen dituen pertsona bizi den herrialdea. Enpresa edo bestelako erakunde bat bada, adierazi egoitza duen herrialdea eta, kasuan kasu, hiria, eskualdea, lurraldea edo estatua. + min_age: Ez luke izan behar zure jurisdikzioko legeek eskatzen duten gutxieneko adinetik beherakoa. user: chosen_languages: Markatzean, hautatutako hizkuntzetan dauden tutak besterik ez dira erakutsiko. + role: Rolak erabiltzaileak dituen baimenak zeintzuk diren kontrolatzen du. user_role: color: Rolarentzat erabiltzaile interfazean erabiliko den kolorea, formatu hamaseitarreko RGB bezala highlighted: Honek rola publikoki ikusgai jartzen du name: Rolaren izen publikoa, rola bereizgarri bezala bistaratzeko ezarrita badago permissions_as_keys: Rol hau duten erabiltzaileek sarbidea izango dute... position: Maila goreneko rolak erabakitzen du gatazkaren konponbidea kasu batzuetan. Ekintza batzuk maila baxuagoko rolen gain bakarrik gauzatu daitezke + username_block: + allow_with_approval: Izena ematea zuzenean eragotzi beharrean, bat datozen erregistroek zure onarpena beharko dute + comparison: Kontuan izan 'Scunthorpe arazoa' hitz-zatiak blokeatzean webhook: events: Hautatu gertaerak bidaltzeko template: Osatu zure JSON karga interpolazio aldakorra erabiliz. Utzi hutsik JSON lehenetsiarentzat. @@ -215,17 +231,26 @@ eu: setting_aggregate_reblogs: Taldekatu bultzadak denbora-lerroetan setting_always_send_emails: Bidali beti eposta jakinarazpenak setting_auto_play_gif: Erreproduzitu GIF animatuak automatikoki + setting_boost_modal: Bultzaden ikusgaitasun-kontrola setting_default_language: Argitalpenen hizkuntza + setting_default_privacy: Bidalketarako ikusgarritasuna + setting_default_quote_policy: Nork aipa dezake setting_default_sensitive: Beti markatu edukiak hunkigarri gisa + setting_delete_modal: Eman abisua argitalpen bat ezabatu aurretik + setting_disable_hover_cards: Desgaitu profilaren aurrebista gainetik igarotzean setting_disable_swiping: Desgaitu hatza pasatzeko mugimenduak setting_display_media: Multimedia bistaratzea setting_display_media_default: Lehenetsia setting_display_media_hide_all: Ezkutatu guztia setting_display_media_show_all: Erakutsi guztia + setting_emoji_style: Emojien estiloa setting_expand_spoilers: Zabaldu beti edukiaren abisuak dituzten argitalpenak setting_hide_network: Ezkutatu zure sarea + setting_missing_alt_text_modal: Eman abisua testu alternatiborik gabeko multimedia-edukia argitaratu aurretik + setting_quick_boosting: Gaitu bultzada azkarra setting_reduce_motion: Murriztu animazioen mugimenduak setting_system_font_ui: Erabili sistemako tipografia lehenetsia + setting_system_scrollbars_ui: Erabili sistemako desplazamendu-barra lehenetsia setting_theme: Gunearen azala setting_trends: Erakutsi gaurko joerak setting_unfollow_modal: Erakutsi baieztapen elkarrizketa-koadroa inor jarraitzeari utzi aurretik @@ -244,6 +269,7 @@ eu: name: Traola filters: actions: + blur: Ezkutatu multimedia-edukia ohar batekin hide: Ezkutatu guztiz warn: Ezkutatu ohar batekin form_admin_settings: @@ -255,12 +281,17 @@ eu: content_cache_retention_period: Urruneko edukiaren atxikipen-aldia custom_css: CSS pertsonalizatua favicon: Gune-ikurra + landing_page: Bisitari berrientzako harrera-orria + local_live_feed_access: Tokiko argitalpenak nabarmentzen dituzten zuzeneko jarioetara sarbidea + local_topic_feed_access: Tokiko argitalpenak nabarmentzen dituzten traola eta esteketara sarbidea mascot: Maskota pertsonalizatua (zaharkitua) media_cache_retention_period: Multimediaren cachea atxikitzeko epea min_age: Gutxieneko adin-eskakizuna peers_api_enabled: Argitaratu aurkitutako zerbitzarien zerrenda APIan profile_directory: Gaitu profil-direktorioa registrations_mode: Nork eman dezake izena + remote_live_feed_access: Urruneko argitalpenak nabarmentzen dituzten zuzeneko jarioetara sarbidea + remote_topic_feed_access: Urruneko argitalpenak nabarmentzen dituzten traola eta esteketara sarbidea require_invite_text: Eskatu arrazoi bat batzeko show_domain_blocks: Erakutsi domeinu-blokeoak show_domain_blocks_rationale: Erakutsi domeinuak zergatik blokeatu ziren @@ -275,6 +306,7 @@ eu: thumbnail: Zerbitzariaren koadro txikia trendable_by_default: Onartu joerak aurrez berrikusi gabe trends: Gaitu joerak + wrapstodon: Gaitu Wrapstodon interactions: must_be_follower: Blokeatu jarraitzaile ez direnen jakinarazpenak must_be_following: Blokeatu zuk jarraitzen ez dituzu horien jakinarazpenak @@ -299,6 +331,7 @@ eu: follow_request: Bidali e-mail bat norbaitek zu jarraitzea eskatzen duenean mention: Bidali e-mail bat norbaitek zu aipatzean pending_account: Bidali e-mail bat kontu bat berrikusi behar denean + quote: Norbaitek aipatu zaitu reblog: Bidali e-mail bat norbaitek zure mezuari bultzada ematen badio report: Salaketa berria bidali da software_updates: @@ -321,11 +354,22 @@ eu: usable: Baimendu bidalketek traola lokal hau erabiltzea terms_of_service: changelog: Zer aldatu da? + effective_date: Indarrean sartzeko data text: Zerbitzuaren baldintzak terms_of_service_generator: + admin_email: Lege-oharretarako helbide elektronikoa + arbitration_address: Arbitraje-jakinarazpenak bidaltzeko helbide fisikoa + arbitration_website: Arbitraje-jakinarazpenak bidaltzeko webgunea + choice_of_law: Aplikatu beharreko legedia + dmca_address: DMCA/egile-eskubideen jakinarazpenetarako helbide fisikoa + dmca_email: DMCA/egile-eskubideen jakinarazpenetarako helbide elektronikoa domain: Domeinua + jurisdiction: Jurisdikzio legala + min_age: Gutxieneko adina user: + date_of_birth_1i: Urtea date_of_birth_2i: Hilabetea + date_of_birth_3i: Eguna role: Rola time_zone: Ordu zona user_role: @@ -334,6 +378,10 @@ eu: name: Izena permissions_as_keys: Baimenak position: Lehentasuna + username_block: + allow_with_approval: Baimendu izen-emateak onarpen bidez + comparison: Konparazio-metodoa + username: Bat etorri beharreko hitza webhook: events: Gertaerak gaituta template: Karga txantiloia diff --git a/config/locales/simple_form.fi.yml b/config/locales/simple_form.fi.yml index a89f4a86aaad9c..a57a5305d88b7d 100644 --- a/config/locales/simple_form.fi.yml +++ b/config/locales/simple_form.fi.yml @@ -312,6 +312,7 @@ fi: thumbnail: Palvelimen pienoiskuva trendable_by_default: Salli trendit ilman ennakkotarkastusta trends: Ota trendit käyttöön + wrapstodon: Ota Wrapstodon käyttöön interactions: must_be_follower: Estä ilmoitukset käyttäjiltä, jotka eivät seuraa sinua must_be_following: Estä ilmoitukset käyttäjiltä, joita et seuraa diff --git a/config/locales/simple_form.fr-CA.yml b/config/locales/simple_form.fr-CA.yml index 265782086c92f8..aec2a254a1ff6a 100644 --- a/config/locales/simple_form.fr-CA.yml +++ b/config/locales/simple_form.fr-CA.yml @@ -111,6 +111,7 @@ fr-CA: thumbnail: Une image d'environ 2:1 affichée à côté des informations de votre serveur. trendable_by_default: Ignorer l'examen manuel du contenu tendance. Des éléments individuels peuvent toujours être supprimés des tendances après coup. trends: Les tendances montrent quelles publications, hashtags et actualités sont en train de gagner en traction sur votre serveur. + wrapstodon: Offrez aux comptes locaux de générer un récapitulatif annuel de leur utilisation de Mastodon. Cette fonctionnalité est disponible chaque année du 10 au 31 décembre, et est accessible pour les comptes ayant publié au moins un message Public ou Public discret et utilisé au moins un hashtag dans l'année. form_challenge: current_password: Vous entrez une zone sécurisée imports: @@ -312,6 +313,7 @@ fr-CA: thumbnail: Miniature du serveur trendable_by_default: Autoriser les tendances sans révision préalable trends: Activer les tendances + wrapstodon: Activer Wrapstodon interactions: must_be_follower: Bloquer les notifications des personnes qui ne vous suivent pas must_be_following: Bloquer les notifications des personnes que vous ne suivez pas diff --git a/config/locales/simple_form.fr.yml b/config/locales/simple_form.fr.yml index a40211f36d657a..77f0917fd8ec4e 100644 --- a/config/locales/simple_form.fr.yml +++ b/config/locales/simple_form.fr.yml @@ -111,6 +111,7 @@ fr: thumbnail: Une image d'environ 2:1 affichée à côté des informations de votre serveur. trendable_by_default: Ignorer l'examen manuel du contenu tendance. Des éléments individuels peuvent toujours être supprimés des tendances après coup. trends: Les tendances montrent quels messages, hashtags et actualités gagnent en popularité sur votre serveur. + wrapstodon: Offrez aux comptes locaux de générer un récapitulatif annuel de leur utilisation de Mastodon. Cette fonctionnalité est disponible chaque année du 10 au 31 décembre, et est accessible pour les comptes ayant publié au moins un message Public ou Public discret et utilisé au moins un hashtag dans l'année. form_challenge: current_password: Vous entrez une zone sécurisée imports: @@ -312,6 +313,7 @@ fr: thumbnail: Miniature du serveur trendable_by_default: Autoriser les tendances sans révision préalable trends: Activer les tendances + wrapstodon: Activer Wrapstodon interactions: must_be_follower: Bloquer les notifications des personnes qui ne vous suivent pas must_be_following: Bloquer les notifications des personnes que vous ne suivez pas diff --git a/config/locales/simple_form.ga.yml b/config/locales/simple_form.ga.yml index b3d6081ffd762b..f8f8b09000c944 100644 --- a/config/locales/simple_form.ga.yml +++ b/config/locales/simple_form.ga.yml @@ -111,6 +111,7 @@ ga: thumbnail: Íomhá thart ar 2:1 ar taispeáint taobh le faisnéis do fhreastalaí. trendable_by_default: Léim ar athbhreithniú láimhe ar ábhar treochta. Is féidir míreanna aonair a bhaint as treochtaí fós tar éis an fhíric. trends: Léiríonn treochtaí cé na postálacha, hashtags agus scéalta nuachta atá ag tarraingt ar do fhreastalaí. + wrapstodon: Iarr ar úsáideoirí áitiúla achoimre spraíúil a ghiniúint ar a n-úsáid Mastodon i rith na bliana. Bíonn an ghné seo ar fáil idir an 10ú agus an 31ú Nollaig gach bliain, agus tairgtear é d’úsáideoirí a rinne post Poiblí nó Ciúin Poiblí amháin ar a laghad agus a d’úsáid hais clib amháin ar a laghad laistigh den bhliain. form_challenge: current_password: Tá tú ag dul isteach i limistéar slán imports: @@ -315,6 +316,7 @@ ga: thumbnail: Mionsamhail freastalaí trendable_by_default: Ceadaigh treochtaí gan athbhreithniú roimh ré trends: Cumasaigh treochtaí + wrapstodon: Cumasaigh Wrapstodon interactions: must_be_follower: Cuir bac ar fhógraí ó dhaoine nach leantóirí iad must_be_following: Cuir bac ar fhógraí ó dhaoine nach leanann tú diff --git a/config/locales/simple_form.gl.yml b/config/locales/simple_form.gl.yml index b89696805718c6..f9d896a2fdca96 100644 --- a/config/locales/simple_form.gl.yml +++ b/config/locales/simple_form.gl.yml @@ -111,6 +111,7 @@ gl: thumbnail: Imaxe con proporcións 2:1 mostrada xunto á información sobre o servidor. trendable_by_default: Omitir a revisión manual dos contidos populares. Poderás igualmente eliminar manualmente os elementos que vaian aparecendo. trends: As tendencias mostran publicacións, cancelos e novas historias que teñen popularidade no teu servidor. + wrapstodon: Ofrecerlle ás usuarias locais crear un divertido resumo do seu uso de Mastodon durante o ano. Esta ferramenta está dispoñible entre o 10 e o 31 de Decembro de cada ano, e ofréceselle ás usuarias que publicaron polo menos unha mensaxe Pública ou Pública Limitada e utilizaron polo menos un cancelo durante o ano. form_challenge: current_password: Estás entrando nun área segura imports: @@ -312,6 +313,7 @@ gl: thumbnail: Icona do servidor trendable_by_default: Permitir tendencias sen aprobación previa trends: Activar tendencias + wrapstodon: Activar Wrapstodon interactions: must_be_follower: Bloquea as notificacións de persoas que non te seguen must_be_following: Bloquea as notificacións de persoas que non segues diff --git a/config/locales/simple_form.he.yml b/config/locales/simple_form.he.yml index cdf1300a4fb9d1..5fcaeb4421ad42 100644 --- a/config/locales/simple_form.he.yml +++ b/config/locales/simple_form.he.yml @@ -111,6 +111,7 @@ he: thumbnail: תמונה ביחס 2:1 בערך שתוצג ליד המידע על השרת שלך. trendable_by_default: לדלג על בדיקה ידנית של התכנים החמים. פריטים ספציפיים עדיין ניתנים להסרה לאחר מעשה. trends: נושאים חמים יציגו אילו הודעות, תגיות וידיעות חדשות צוברות חשיפה על השרת שלך. + wrapstodon: אפשר למשתמשיך המקומיים.ות ליצור סיכום חביב של פעילותם במסטודון בשנה האחרונה. התכונה מאופשרת בין 10 ועד 31 בדצמבר כל שנה, ומצעת למשתמשים שיצרו לפחות הודעה ציבורית אחת והשתמשו לפחות בתגית אחת במשך השנה. form_challenge: current_password: את.ה נכנס. ת לאזור מאובטח imports: @@ -314,6 +315,7 @@ he: thumbnail: תמונה ממוזערת מהשרת trendable_by_default: הרשאה לפריטים להופיע בנושאים החמים ללא אישור מוקדם trends: אפשר פריטים חמים (טרנדים) + wrapstodon: הפעלת סיכומודון interactions: must_be_follower: חסימת התראות משאינם עוקבים must_be_following: חסימת התראות משאינם נעקבים diff --git a/config/locales/simple_form.hu.yml b/config/locales/simple_form.hu.yml index aa118bf5d6cc70..6021d66f6506f9 100644 --- a/config/locales/simple_form.hu.yml +++ b/config/locales/simple_form.hu.yml @@ -111,6 +111,7 @@ hu: thumbnail: Egy durván 2:1 arányú kép, amely a kiszolgálóinformációk mellett jelenik meg. trendable_by_default: Kézi felülvizsgálat kihagyása a felkapott tartalmaknál. Az egyes elemek utólag távolíthatók el a trendek közül. trends: A trendek azt mondják meg, hogy mely bejegyzések, hashtagek és hírbejegyzések felkapottak a kiszolgálódon. + wrapstodon: Kínáld fel a lehetőséget a helyi felhasználóknak, hogy egy vidám összefoglalóban összesíthessék a Mastodon használatukat az év folyamán. Ez a lehetőség minden évben december 10. és 31. között érhető el, azoknak, aki létrehoztak legalább egy nyilvános vagy csendes nyilvános bejegyzést és használtak legalább egy hashtaget az év folyamán. form_challenge: current_password: Beléptél egy biztonsági térben imports: @@ -312,6 +313,7 @@ hu: thumbnail: Kiszolgáló bélyegképe trendable_by_default: Trendek engedélyezése előzetes ellenőrzés nélkül trends: Trendek engedélyezése + wrapstodon: Wrapstodon engedélyezése interactions: must_be_follower: Nem követőidtől érkező értesítések tiltása must_be_following: Nem követettjeidtől érkező értesítések tiltása diff --git a/config/locales/simple_form.is.yml b/config/locales/simple_form.is.yml index b4f034bda4b39c..cffa7449d35731 100644 --- a/config/locales/simple_form.is.yml +++ b/config/locales/simple_form.is.yml @@ -111,6 +111,7 @@ is: thumbnail: Mynd um það bil 2:1 sem birtist samhliða upplýsingum um netþjóninn þinn. trendable_by_default: Sleppa handvirkri yfirferð á vinsælu efni. Áfram verður hægt að fjarlægja stök atriði úr vinsældarlistum. trends: Vinsældir sýna hvaða færslur, myllumerki og fréttasögur séu í umræðunni á netþjóninum þínum. + wrapstodon: Býður notendum á netþjóninum upp á skemmtilega samantekt um notkun þeirra á Mastodon á árinu sem er að líða. Þessi eiginleiki er í boði á milli 10. og 31. desember ár hvert og býðst þeim notendum sem hafa gert að minnsta kosti eina opinbera eða hljóðlega opinbera færslu og notað a. m. k. eitt myllumerki á árinu. form_challenge: current_password: Þú ert að fara inn á öryggissvæði imports: @@ -312,6 +313,7 @@ is: thumbnail: Smámynd vefþjóns trendable_by_default: Leyfa vinsælt efni án undanfarandi yfirferðar trends: Virkja vinsælt + wrapstodon: Virkja Ársuppgjörið interactions: must_be_follower: Loka á tilkynningar frá þeim sem ekki eru fylgjendur must_be_following: Loka á tilkynningar frá þeim sem þú fylgist ekki með diff --git a/config/locales/simple_form.it.yml b/config/locales/simple_form.it.yml index 9dd05272b7366b..98772991814ba7 100644 --- a/config/locales/simple_form.it.yml +++ b/config/locales/simple_form.it.yml @@ -111,6 +111,7 @@ it: thumbnail: Un'immagine approssimativamente 2:1 visualizzata insieme alle informazioni del tuo server. trendable_by_default: Salta la revisione manuale dei contenuti di tendenza. I singoli elementi possono ancora essere rimossi dalle tendenze dopo il fatto. trends: Le tendenze mostrano quali post, hashtag e notizie stanno guadagnando popolarità sul tuo server. + wrapstodon: Offri agli utenti locali la possibilità di generare un riassunto giocoso del loro utilizzo di Mastodon durante l’anno. Questa funzione è disponibile dal 10 al 31 dicembre di ogni anno ed è offerta agli utenti che hanno pubblicato almeno un post pubblico o pubblico silenzioso e utilizzato almeno un hashtag nell’arco dell’anno. form_challenge: current_password: Stai entrando in un'area sicura imports: @@ -312,6 +313,7 @@ it: thumbnail: Miniatura del server trendable_by_default: Consenti le tendenze senza revisione preventiva trends: Abilita le tendenze + wrapstodon: Abilita Wrapstodon interactions: must_be_follower: Blocca notifiche da chi non ti segue must_be_following: Blocca notifiche dalle persone che non segui diff --git a/config/locales/simple_form.ko.yml b/config/locales/simple_form.ko.yml index 2f0e09bc94d1ea..5d96fd07215dfb 100644 --- a/config/locales/simple_form.ko.yml +++ b/config/locales/simple_form.ko.yml @@ -54,6 +54,7 @@ ko: password: 최소 8글자 phrase: 게시물 내용이나 열람주의 내용 안에서 대소문자 구분 없이 매칭 됩니다 scopes: 애플리케이션에 허용할 API들입니다. 최상위 스코프를 선택하면 개별적인 것은 선택하지 않아도 됩니다. + setting_advanced_layout: 마스토돈을 멀티컬럼으로 보여주어 타임라인, 알림, 그리고 내가 원하는 컬럼을 한 번에 볼 수 있도록 합니다. 작은 화면에선 추천하지 않습니다. setting_aggregate_reblogs: 최근에 부스트 됐던 게시물은 새로 부스트 되어도 보여주지 않기 (새로 받은 부스트에만 적용됩니다) setting_always_send_emails: 기본적으로 마스토돈을 활동적으로 사용하고 있을 때에는 이메일 알림이 보내지지 않습니다 setting_boost_modal: 활성화하면 부스트하기 전에 부스트의 공개설정을 바꿀 수 있는 확인창이 먼저 뜨게 됩니다. diff --git a/config/locales/simple_form.pt-PT.yml b/config/locales/simple_form.pt-PT.yml index 608e27a90fbe71..339c48edea8294 100644 --- a/config/locales/simple_form.pt-PT.yml +++ b/config/locales/simple_form.pt-PT.yml @@ -111,6 +111,7 @@ pt-PT: thumbnail: Uma imagem de cerca de 2:1, apresentada ao lado da informação do seu servidor. trendable_by_default: Ignorar a revisão manual do conteúdo em destaque. Os itens individuais poderão ainda assim ser posteriormente removidos das tendências. trends: As tendências mostram quais as publicações, etiquetas e notícias que estão a ganhar destaque no seu servidor. + wrapstodon: Ofereça aos utilizadores locais a possibilidade de gerar um resumo divertido da sua utilização do Mastodon durante o ano. Esta funcionalidade está disponível entre 10 e 31 de Dezembro de cada ano e é oferecida aos utilizadores que fizeram pelo menos uma publicação Pública ou Não listada e utilizaram pelo menos uma etiqueta durante o ano. form_challenge: current_password: Está a entrar numa área segura imports: @@ -312,6 +313,7 @@ pt-PT: thumbnail: Miniatura do servidor trendable_by_default: Permitir tendências sem revisão prévia trends: Ativar destaques + wrapstodon: Ativar Wrapstodon interactions: must_be_follower: Bloquear notificações de não-seguidores must_be_following: Bloquear notificações de pessoas que não segues diff --git a/config/locales/simple_form.sq.yml b/config/locales/simple_form.sq.yml index 346044f7313bf1..acdb944ea3987c 100644 --- a/config/locales/simple_form.sq.yml +++ b/config/locales/simple_form.sq.yml @@ -110,6 +110,7 @@ sq: thumbnail: Një figurë afërsisht 2:1 e shfaqur tok me hollësi mbi shërbyesin tuaj. trendable_by_default: Anashkalo shqyrtim dorazi lënde në modë. Gjëra individuale prapë mund të hiqen nga lëndë në modë pas publikimi. trends: Gjërat në modë shfaqin cilat postime, hashtagë dhe histori të reja po tërheqin vëmendjen në shërbyesin tuaj. + wrapstodon: Jepuni përdoruesve vendorë mundësinë të prodhojnë një përmbledhje lojcake të përdorimit të Mastodon-it prej tyre përgjatë vitit. Kjo veçori është e përdorshme mes 10 dhe 31 dhjetorit të çdo viti dhe u ofrohet përdoruesve që kanë për të paktën një postim Publik, ose Publik të Heshtur dhe që kanë përdorur të paktën një hashtag brenda vitit. form_challenge: current_password: Po hyni në një zonë të sigurt imports: @@ -311,6 +312,7 @@ sq: thumbnail: Miniaturë shërbyesi trendable_by_default: Lejoni gjëra në modë pa shqyrtim paraprak trends: Aktivizo gjëra në modë + wrapstodon: Aktivizo Përmbledhjedon-in interactions: must_be_follower: Blloko njoftime nga jo-ndjekës must_be_following: Blloko njoftime nga persona që s’i ndiqni diff --git a/config/locales/simple_form.sv.yml b/config/locales/simple_form.sv.yml index b7089ede18fd68..7699224561f2a3 100644 --- a/config/locales/simple_form.sv.yml +++ b/config/locales/simple_form.sv.yml @@ -305,6 +305,7 @@ sv: thumbnail: Serverns tumnagelbild trendable_by_default: Tillåt trender utan föregående granskning trends: Aktivera trender + wrapstodon: Aktivera Wrapstodon interactions: must_be_follower: Blockera notiser från icke-följare must_be_following: Blockera notiser från personer du inte följer diff --git a/config/locales/simple_form.tr.yml b/config/locales/simple_form.tr.yml index 0e79c05766611b..35df26dc6e57a9 100644 --- a/config/locales/simple_form.tr.yml +++ b/config/locales/simple_form.tr.yml @@ -111,6 +111,7 @@ tr: thumbnail: Sunucu bilginizin yanında gösterilen yaklaşık 2:1'lik görüntü. trendable_by_default: Öne çıkan içeriğin elle incelenmesini atla. Tekil öğeler sonrada öne çıkanlardan kaldırılabilir. trends: Öne çıkanlar, sunucunuzda ilgi toplayan gönderileri, etiketleri ve haber yazılarını gösterir. + wrapstodon: Yerel kullanıcılara, yıl boyunca Mastodon kullanımlarının eğlenceli bir özetini oluşturma imkanı sunun. Bu özellik, her yıl 10 Aralık ile 31 Aralık tarihleri arasında kullanılabilir ve yıl içinde en az bir adet Halka Açık veya Sessiz Halka Açık gönderi paylaşan ve en az bir hashtag kullanan kullanıcılara sunulur. form_challenge: current_password: Güvenli bir bölgeye giriyorsunuz imports: @@ -312,6 +313,7 @@ tr: thumbnail: Sunucu küçük resmi trendable_by_default: Ön incelemesiz öne çıkanlara izin ver trends: Öne çıkanları etkinleştir + wrapstodon: Wrapstodonu Etkinleştir interactions: must_be_follower: Takipçim olmayan kişilerden gelen bildirimleri engelle must_be_following: Takip etmediğim kişilerden gelen bildirimleri engelle diff --git a/config/locales/simple_form.vi.yml b/config/locales/simple_form.vi.yml index 2c84279da2ed64..027f00bf8b6887 100644 --- a/config/locales/simple_form.vi.yml +++ b/config/locales/simple_form.vi.yml @@ -111,6 +111,7 @@ vi: thumbnail: 'Một hình ảnh tỉ lệ 2: 1 được hiển thị cùng với thông tin máy chủ của bạn.' trendable_by_default: Bỏ qua việc duyệt thủ công nội dung xu hướng. Các mục riêng lẻ vẫn có thể bị xóa khỏi xu hướng sau này. trends: Hiển thị những tút, hashtag và tin tức đang được thảo luận nhiều trên máy chủ của bạn. + wrapstodon: Cho phép người dùng máy chủ tạo bản tóm tắt vui nhộn về việc sử dụng Mastodon của họ trong năm. Tính năng này có sẵn từ ngày 10 đến ngày 31 tháng 12 hàng năm, và được cung cấp cho người dùng đã đăng ít nhất một tút Công khai hoặc Riêng tư và sử dụng ít nhất một hashtag trong năm. form_challenge: current_password: Biểu mẫu này an toàn imports: @@ -311,6 +312,7 @@ vi: thumbnail: Hình thu nhỏ của máy chủ trendable_by_default: Cho phép lên xu hướng mà không cần duyệt trước trends: Bật xu hướng + wrapstodon: Bật Wrapstodon interactions: must_be_follower: Những người không theo dõi bạn must_be_following: Những người bạn không theo dõi diff --git a/config/locales/simple_form.zh-CN.yml b/config/locales/simple_form.zh-CN.yml index d5c4525233a49b..0bc0ffa4135bb3 100644 --- a/config/locales/simple_form.zh-CN.yml +++ b/config/locales/simple_form.zh-CN.yml @@ -111,6 +111,7 @@ zh-CN: thumbnail: 与服务器信息一并展示的约 2:1 比例的图像。 trendable_by_default: 跳过对热门内容的手工审核。个别项目仍可在之后从趋势中删除。 trends: 热门页中会显示正在你服务器上受到关注的嘟文、标签和新闻故事。 + wrapstodon: 为本站用户提供生成他们过去一年使用 Mastodon 情况的趣味总结的功能。此功能在每年12月10日至12月31日提供给这一年发布过至少1条公开嘟文(无论是否设置为在时间线上显示)及至少使用过1个话题标签的用户。 form_challenge: current_password: 你正在进入安全区域 imports: @@ -311,6 +312,7 @@ zh-CN: thumbnail: 本站缩略图 trendable_by_default: 允许在未审核的情况下将话题置为热门 trends: 启用热门 + wrapstodon: 启用 Wrapstodon 年度回顾 interactions: must_be_follower: 屏蔽来自未关注我的用户的通知 must_be_following: 屏蔽来自我未关注的用户的通知 diff --git a/config/locales/simple_form.zh-TW.yml b/config/locales/simple_form.zh-TW.yml index e371d0ac077b23..f01c2741d3b5a6 100644 --- a/config/locales/simple_form.zh-TW.yml +++ b/config/locales/simple_form.zh-TW.yml @@ -109,8 +109,9 @@ zh-TW: status_page_url: 當服務中斷時,可以提供使用者了解伺服器資訊頁面之 URL theme: 未登入之訪客或新使用者所見之佈景主題。 thumbnail: 大約 2:1 圖片會顯示於您伺服器資訊之旁。 - trendable_by_default: 跳過手動審核熱門內容。仍能於登上熱門趨勢後移除個別內容。 - trends: 熱門趨勢將顯示於您伺服器上正在吸引大量注意力的嘟文、主題標籤、或者新聞。 + trendable_by_default: 跳過手動審核熱門內容。您仍能於登上熱門趨勢後移除個別內容。 + trends: 熱門趨勢將顯示於您伺服器上正在吸引大量注意力之嘟文、主題標籤、或新聞。 + wrapstodon: 提供替本站使用者產生他們過去一年使用 Mastodon 的趣味總結。此功能於每年十二月十號至三十一號提供至於年中發過至少一則公開嘟文(無論顯示於時間軸與否)及至少使用一則主題標籤之使用者。 form_challenge: current_password: 您正要進入安全區域 imports: @@ -311,6 +312,7 @@ zh-TW: thumbnail: 伺服器縮圖 trendable_by_default: 允許熱門趨勢直接顯示,不需經過審核 trends: 啟用熱門趨勢 + wrapstodon: 啟用 Mastodon 年度回顧 interactions: must_be_follower: 封鎖非跟隨者的通知 must_be_following: 封鎖您未跟隨之使用者的通知 @@ -354,7 +356,7 @@ zh-TW: tag: listable: 允許此主題標籤於搜尋及個人檔案目錄中顯示 name: 主題標籤 - trendable: 允許此主題標籤於熱門趨勢下顯示 + trendable: 允許此主題標籤於熱門趨勢中顯示 usable: 允許嘟文使用此主題標籤 terms_of_service: changelog: 有何異動? diff --git a/config/locales/sq.yml b/config/locales/sq.yml index 8171dcea85c069..6c64d4dec53a5f 100644 --- a/config/locales/sq.yml +++ b/config/locales/sq.yml @@ -7,6 +7,8 @@ sq: hosted_on: Server Mastodon i strehuar në %{domain} title: Mbi accounts: + errors: + cannot_be_added_to_collections: Kjo llogari s’mund të shtohet në koleksione. followers: one: Ndjekës other: Ndjekës @@ -838,6 +840,7 @@ sq: publish_statistics: Publiko statistika title: Zbulim trends: Në modë + wrapstodon: Përmbledhjedon domain_blocks: all: Për këdo disabled: Për askënd @@ -2169,3 +2172,5 @@ sq: not_supported: Ky shfletues nuk mbulon kyçe sigurie otp_required: Që të përdoren kyçe sigurie, ju lutemi, së pari aktivizoni mirëfilltësimin dyfaktorësh. registered_on: Regjistruar më %{date} + wrapstodon: + description: Shihni si u përdor Mastodon-in këtë vit nga %{name}! diff --git a/config/locales/sv.yml b/config/locales/sv.yml index 58fc19951850d7..1f6d68750cb5bc 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -846,6 +846,7 @@ sv: publish_statistics: Publicera statistik title: Upptäck trends: Trender + wrapstodon: Wrapstodon domain_blocks: all: Till alla disabled: För ingen diff --git a/config/locales/tr.yml b/config/locales/tr.yml index 749088b7191d16..0eb31119e8e8a1 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -7,6 +7,8 @@ tr: hosted_on: Mastodon %{domain} üzerinde barındırılıyor title: Hakkında accounts: + errors: + cannot_be_added_to_collections: Bu hesap koleksiyonlara eklenemez. followers: one: Takipçi other: Takipçiler @@ -846,6 +848,7 @@ tr: publish_statistics: İstatistikleri yayınla title: Keşfet trends: Öne çıkanlar + wrapstodon: Wrapstodon domain_blocks: all: Herkes için disabled: Hiç kimseye @@ -2186,4 +2189,5 @@ tr: otp_required: Güvenlik anahtarlarını kullanmak için lütfen önce iki adımlı kimlik doğrulamayı etkinleştirin. registered_on: "%{date} tarihinde kaydoldu" wrapstodon: + description: "%{name} kullanıcısının bu yıl Mastodon'u nasıl kullandığını görün!" title: "%{name} için %{year} Wrapstodon'u" diff --git a/config/locales/vi.yml b/config/locales/vi.yml index 59bd33fffd9525..035ef775ff0d36 100644 --- a/config/locales/vi.yml +++ b/config/locales/vi.yml @@ -7,6 +7,8 @@ vi: hosted_on: "%{domain} vận hành nhờ Mastodon" title: Giới thiệu accounts: + errors: + cannot_be_added_to_collections: Tài khoản này không thể thêm vào bộ sưu tập. followers: other: Người theo dõi following: Theo dõi @@ -832,6 +834,7 @@ vi: publish_statistics: Công khai số liệu thống kê title: Khám phá trends: Xu hướng + wrapstodon: Wrapstodon domain_blocks: all: Tới mọi người disabled: Không ai @@ -2142,4 +2145,5 @@ vi: otp_required: Để dùng khóa bảo mật, trước tiên hãy kích hoạt xác thực 2 bước. registered_on: Đăng ký vào %{date} wrapstodon: + description: Xem năm nay %{name} đã dùng Mastodon như thế nào! title: Wrapstodon %{year} cho %{name} diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index bfe2c9917ad9a3..87356403d2bc3b 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -7,6 +7,8 @@ zh-CN: hosted_on: 运行在 %{domain} 上的 Mastodon 实例 title: 关于本站 accounts: + errors: + cannot_be_added_to_collections: 此账号无法被添加到关注列表内。 followers: other: 关注者 following: 正在关注 @@ -832,6 +834,7 @@ zh-CN: publish_statistics: 发布统计数据 title: 发现 trends: 热门 + wrapstodon: Wrapstodon 年度回顾 domain_blocks: all: 对每个人 disabled: 不对任何人 @@ -2142,4 +2145,5 @@ zh-CN: otp_required: 要使用安全密钥,请先启用双因素认证。 registered_on: 注册于 %{date} wrapstodon: + description: 来看看%{name}今年是怎么使用Mastodon的! title: "%{name}的%{year}年Wrapstodon年度回顾" diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index ec19a2424769f1..7c71c2487adfe5 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -7,6 +7,8 @@ zh-TW: hosted_on: 於 %{domain} 託管之 Mastodon 站點 title: 關於本站 accounts: + errors: + cannot_be_added_to_collections: 此帳號無法被加至集合中。 followers: other: 跟隨者 following: 正在跟隨 @@ -834,6 +836,7 @@ zh-TW: publish_statistics: 公開統計資料 title: 探索 trends: 熱門趨勢 + wrapstodon: Mastodon 年度回顧 domain_blocks: all: 至任何人 disabled: 至沒有人 @@ -2148,4 +2151,5 @@ zh-TW: otp_required: 請先啟用兩階段驗證以使用安全金鑰。 registered_on: 註冊於 %{date} wrapstodon: + description: 來瞧瞧 %{name} 今年是如何使用 Mastodon 吧! title: "%{name} 的 %{year} Mastodon 年度回顧 (Wrapstodon)" diff --git a/config/roles.yml b/config/roles.yml index 9e6887dda9c2ed..a41a00c5452af8 100644 --- a/config/roles.yml +++ b/config/roles.yml @@ -14,6 +14,7 @@ admin: permissions: - view_dashboard - view_audit_log + - view_feeds - manage_users - manage_user_access - delete_user_data diff --git a/config/routes/api.rb b/config/routes/api.rb index 09524c00ece752..b9116eb8bd6893 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -12,7 +12,9 @@ resources :async_refreshes, only: :show - resources :collections, only: [:show, :create, :update, :destroy] + resources :collections, only: [:show, :create, :update, :destroy] do + resources :items, only: [:create, :destroy], controller: 'collection_items' + end end # JSON / REST API diff --git a/config/settings.yml b/config/settings.yml index b532452c4047bf..07316e43a2951f 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -52,6 +52,7 @@ defaults: &defaults allow_referrer_origin: false auto_accept_legacy_quotes: false landing_page: 'trends' + wrapstodon: true development: <<: *defaults diff --git a/db/migrate/20251209093813_add_item_count_to_collections.rb b/db/migrate/20251209093813_add_item_count_to_collections.rb new file mode 100644 index 00000000000000..071cfab46ab962 --- /dev/null +++ b/db/migrate/20251209093813_add_item_count_to_collections.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddItemCountToCollections < ActiveRecord::Migration[8.0] + def change + add_column :collections, :item_count, :integer, default: 0, null: false + end +end diff --git a/db/migrate/20251217091936_add_feature_approval_policy_to_accounts.rb b/db/migrate/20251217091936_add_feature_approval_policy_to_accounts.rb new file mode 100644 index 00000000000000..6d6e6e820568e1 --- /dev/null +++ b/db/migrate/20251217091936_add_feature_approval_policy_to_accounts.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddFeatureApprovalPolicyToAccounts < ActiveRecord::Migration[8.0] + def change + add_column :accounts, :feature_approval_policy, :integer, null: false, default: 0 + end +end diff --git a/db/schema.rb b/db/schema.rb index 36f3884df87d9e..1f6663d5fba8d2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_12_02_140424) do +ActiveRecord::Schema[8.0].define(version: 2025_12_17_091936) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -206,6 +206,7 @@ t.string "attribution_domains", default: [], array: true t.string "following_url", default: "", null: false t.integer "id_scheme", default: 1 + t.integer "feature_approval_policy", default: 0, null: false t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin t.index "lower((username)::text), COALESCE(lower((domain)::text), ''::text)", name: "index_accounts_on_username_and_domain_lower", unique: true t.index ["domain", "id"], name: "index_accounts_on_domain_and_id" @@ -501,6 +502,7 @@ t.integer "original_number_of_items" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.integer "item_count", default: 0, null: false t.index ["account_id"], name: "index_collections_on_account_id" t.index ["tag_id"], name: "index_collections_on_tag_id" end diff --git a/lib/tasks/emojis.rake b/lib/tasks/emojis.rake index bf43aee439b708..053dfd83bd0063 100644 --- a/lib/tasks/emojis.rake +++ b/lib/tasks/emojis.rake @@ -60,7 +60,7 @@ end namespace :emojis do desc 'Generate a unicode to filename mapping' task :generate do - source = 'http://www.unicode.org/Public/emoji/16.0/emoji-test.txt' + source = 'https://www.unicode.org/Public/emoji/16.0/emoji-test.txt' codes = [] dest = Rails.root.join('app', 'javascript', 'mastodon', 'features', 'emoji', 'emoji_map.json') diff --git a/package.json b/package.json index c13d02fc9364d7..cdeef8299bc6a8 100644 --- a/package.json +++ b/package.json @@ -119,7 +119,7 @@ "stacktrace-js": "^2.0.2", "stringz": "^2.1.0", "substring-trie": "^1.0.2", - "tesseract.js": "^6.0.0", + "tesseract.js": "^7.0.0", "tiny-queue": "^0.2.1", "twitter-text": "3.1.0", "use-debounce": "^10.0.0", @@ -127,7 +127,7 @@ "vite-plugin-manifest-sri": "^0.2.0", "vite-plugin-pwa": "^1.0.2", "vite-plugin-svgr": "^4.3.0", - "vite-tsconfig-paths": "^5.1.4", + "vite-tsconfig-paths": "^6.0.0", "wicg-inert": "^3.1.2", "workbox-expiration": "^7.3.0", "workbox-routing": "^7.3.0", @@ -186,7 +186,7 @@ "lint-staged": "^16.2.6", "msw": "^2.12.1", "msw-storybook-addon": "^2.0.6", - "playwright": "^1.56.1", + "playwright": "^1.57.0", "prettier": "^3.3.3", "react-test-renderer": "^18.2.0", "storybook": "^10.0.5", diff --git a/spec/controllers/follower_accounts_controller_spec.rb b/spec/controllers/follower_accounts_controller_spec.rb index d996761169240a..358341cb7299a3 100644 --- a/spec/controllers/follower_accounts_controller_spec.rb +++ b/spec/controllers/follower_accounts_controller_spec.rb @@ -68,6 +68,26 @@ end end + context 'when request is signed in and user blocks an account' do + let(:account) { Fabricate :account } + + before do + Fabricate :block, account:, target_account: follower_bob + sign_in(account.user) + end + + it 'returns followers without blocked' do + expect(response) + .to have_http_status(200) + expect(response.parsed_body) + .to include( + orderedItems: contain_exactly( + include(follow_from_chris.account.id.to_s) + ) + ) + end + end + context 'when account is permanently suspended' do before do alice.suspend! diff --git a/spec/controllers/following_accounts_controller_spec.rb b/spec/controllers/following_accounts_controller_spec.rb index 576d25d93c85e0..7f11a50395adb3 100644 --- a/spec/controllers/following_accounts_controller_spec.rb +++ b/spec/controllers/following_accounts_controller_spec.rb @@ -68,6 +68,26 @@ end end + context 'when request is signed in and user blocks an account' do + let(:account) { Fabricate :account } + + before do + Fabricate :block, account:, target_account: followee_bob + sign_in(account.user) + end + + it 'returns followers without blocked' do + expect(response) + .to have_http_status(200) + expect(response.parsed_body) + .to include( + orderedItems: contain_exactly( + include(follow_of_chris.target_account.id.to_s) + ) + ) + end + end + context 'when account is permanently suspended' do before do alice.suspend! diff --git a/spec/helpers/admin/account_moderation_notes_helper_spec.rb b/spec/helpers/admin/account_moderation_notes_helper_spec.rb index d8fc0ee233554e..a68c8abba94a50 100644 --- a/spec/helpers/admin/account_moderation_notes_helper_spec.rb +++ b/spec/helpers/admin/account_moderation_notes_helper_spec.rb @@ -17,7 +17,7 @@ end context 'with account' do - let(:account) { Fabricate(:account) } + let(:account) { Fabricate.build(:account, id: 123) } it 'returns a labeled avatar link to the account' do expect(parsed_html.a[:href]).to eq admin_account_path(account.id) @@ -39,7 +39,7 @@ end context 'with account' do - let(:account) { Fabricate(:account) } + let(:account) { Fabricate.build(:account, id: 123) } it 'returns an inline link to the account' do expect(parsed_html.a[:href]).to eq admin_account_path(account.id) diff --git a/spec/helpers/admin/dashboard_helper_spec.rb b/spec/helpers/admin/dashboard_helper_spec.rb index 9c674fb4b9664d..db95eb9f2ce644 100644 --- a/spec/helpers/admin/dashboard_helper_spec.rb +++ b/spec/helpers/admin/dashboard_helper_spec.rb @@ -4,8 +4,9 @@ RSpec.describe Admin::DashboardHelper do describe 'relevant_account_timestamp' do + let(:account) { Fabricate(:account) } + context 'with an account with older sign in' do - let(:account) { Fabricate(:account) } let(:stamp) { 10.days.ago } it 'returns a time element' do @@ -18,8 +19,6 @@ end context 'with an account with newer sign in' do - let(:account) { Fabricate(:account) } - it 'returns a time element' do account.user.update(current_sign_in_at: 10.hours.ago) result = helper.relevant_account_timestamp(account) @@ -29,8 +28,6 @@ end context 'with an account where the user is pending' do - let(:account) { Fabricate(:account) } - it 'returns a time element' do account.user.update(current_sign_in_at: nil) account.user.update(approved: false) @@ -42,7 +39,6 @@ end context 'with an account with a last status value' do - let(:account) { Fabricate(:account) } let(:stamp) { 5.minutes.ago } it 'returns a time element' do @@ -56,8 +52,6 @@ end context 'with an account without sign in or last status or pending' do - let(:account) { Fabricate(:account) } - it 'returns a time element' do account.user.update(current_sign_in_at: nil) result = helper.relevant_account_timestamp(account) diff --git a/spec/helpers/admin/trends/statuses_helper_spec.rb b/spec/helpers/admin/trends/statuses_helper_spec.rb index 6abc4569b41bd6..634b4c94c3904c 100644 --- a/spec/helpers/admin/trends/statuses_helper_spec.rb +++ b/spec/helpers/admin/trends/statuses_helper_spec.rb @@ -54,7 +54,7 @@ context 'with a status that has emoji' do before { Fabricate(:custom_emoji, shortcode: 'florpy') } - let(:status) { Fabricate(:status, text: 'hello there :florpy:') } + let(:status) { Fabricate.build(:status, text: 'hello there :florpy:') } it 'renders a correct preview text' do result = helper.one_line_preview(status) diff --git a/spec/helpers/filters_helper_spec.rb b/spec/helpers/filters_helper_spec.rb new file mode 100644 index 00000000000000..7cbacdfa34b319 --- /dev/null +++ b/spec/helpers/filters_helper_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe FiltersHelper do + describe '#filter_keywords' do + subject { helper.filter_keywords(filter) } + + let(:filter) { Fabricate.build :custom_filter, keywords: } + let(:keywords) { words.map { |keyword| Fabricate.build(:custom_filter_keyword, keyword:) } } + + context 'with few keywords' do + let(:words) { %w(One) } + + it { is_expected.to eq('One') } + end + + context 'with many keywords' do + let(:words) { %w(One Two Three Four Five Six Seven Eight Nine Ten) } + + it { is_expected.to eq('One, Two, Three, Four, Five, …') } + end + end +end diff --git a/spec/helpers/formatting_helper_spec.rb b/spec/helpers/formatting_helper_spec.rb index 5ff534e4eb3873..4e605850c1a176 100644 --- a/spec/helpers/formatting_helper_spec.rb +++ b/spec/helpers/formatting_helper_spec.rb @@ -18,7 +18,7 @@ end context 'with a spoiler and an emoji and a poll' do - let(:status) { Fabricate(:status, text: 'Hello :world: <>', spoiler_text: 'This is a spoiler<>', poll: Fabricate(:poll, options: %w(Yes<> No))) } + let(:status) { Fabricate(:status, text: 'Hello :world: <>', spoiler_text: 'This is a spoiler<>', poll: Fabricate.build(:poll, options: %w(Yes<> No))) } before { Fabricate :custom_emoji, shortcode: 'world' } diff --git a/spec/helpers/home_helper_spec.rb b/spec/helpers/home_helper_spec.rb index a056eae364dfb2..e63c03528c05c1 100644 --- a/spec/helpers/home_helper_spec.rb +++ b/spec/helpers/home_helper_spec.rb @@ -21,7 +21,7 @@ end context 'with a valid account' do - let(:account) { Fabricate(:account) } + let(:account) { Fabricate.build(:account) } before { helper.extend controller_helpers } diff --git a/spec/helpers/media_component_helper_spec.rb b/spec/helpers/media_component_helper_spec.rb index a44b9b841509b9..60c9f84da219a8 100644 --- a/spec/helpers/media_component_helper_spec.rb +++ b/spec/helpers/media_component_helper_spec.rb @@ -5,8 +5,10 @@ RSpec.describe MediaComponentHelper do before { helper.extend controller_helpers } + let(:media) { Fabricate.build(:media_attachment, type:, status: Fabricate.build(:status)) } + describe 'render_video_component' do - let(:media) { Fabricate(:media_attachment, type: :video, status: Fabricate(:status)) } + let(:type) { :video } let(:result) { helper.render_video_component(media.status) } it 'renders a react component for the video' do @@ -15,7 +17,7 @@ end describe 'render_audio_component' do - let(:media) { Fabricate(:media_attachment, type: :audio, status: Fabricate(:status)) } + let(:type) { :audio } let(:result) { helper.render_audio_component(media.status) } it 'renders a react component for the audio' do @@ -24,7 +26,7 @@ end describe 'render_media_gallery_component' do - let(:media) { Fabricate(:media_attachment, type: :audio, status: Fabricate(:status)) } + let(:type) { :audio } let(:result) { helper.render_media_gallery_component(media.status) } it 'renders a react component for the media gallery' do diff --git a/spec/helpers/statuses_helper_spec.rb b/spec/helpers/statuses_helper_spec.rb index fd21910a631934..c42cc3170c6826 100644 --- a/spec/helpers/statuses_helper_spec.rb +++ b/spec/helpers/statuses_helper_spec.rb @@ -24,16 +24,13 @@ end describe '#media_summary' do - it 'describes the media on a status' do - status = Fabricate :status - Fabricate :media_attachment, status: status, type: :video - Fabricate :media_attachment, status: status, type: :audio - Fabricate :media_attachment, status: status, type: :image + subject { helper.media_summary(status) } - result = helper.media_summary(status) + let(:status) { Fabricate.build :status } - expect(result).to eq('Attached: 1 image · 1 video · 1 audio') - end + before { %i(video audio image).each { |type| Fabricate.build :media_attachment, status:, type: } } + + it { is_expected.to eq('Attached: 1 image · 1 video · 1 audio') } end describe 'visibility_icon' do diff --git a/spec/lib/activitypub/activity/quote_request_spec.rb b/spec/lib/activitypub/activity/quote_request_spec.rb index aae4ce0338329f..db80448a80b6a4 100644 --- a/spec/lib/activitypub/activity/quote_request_spec.rb +++ b/spec/lib/activitypub/activity/quote_request_spec.rb @@ -87,7 +87,7 @@ context 'when trying to quote a quotable local status' do before do stub_request(:get, 'https://example.com/unknown-status').to_return(status: 200, body: Oj.dump(status_json), headers: { 'Content-Type': 'application/activity+json' }) - quoted_post.update(quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16) + quoted_post.update(quote_approval_policy: InteractionPolicy::POLICY_FLAGS[:public] << 16) end it 'accepts the quote and sends an Accept activity' do @@ -105,7 +105,7 @@ let(:instrument) { status_json.without('@context') } before do - quoted_post.update(quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16) + quoted_post.update(quote_approval_policy: InteractionPolicy::POLICY_FLAGS[:public] << 16) end it 'accepts the quote and sends an Accept activity' do diff --git a/spec/lib/activitypub/parser/interaction_policy_parser_spec.rb b/spec/lib/activitypub/parser/interaction_policy_parser_spec.rb new file mode 100644 index 00000000000000..0af445eab20522 --- /dev/null +++ b/spec/lib/activitypub/parser/interaction_policy_parser_spec.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::Parser::InteractionPolicyParser do + subject { described_class.new(json_policy, account) } + + let(:account) do + Fabricate(:account, + uri: 'https://foo.test', + domain: 'foo.test', + followers_url: 'https://foo.test/followers', + following_url: 'https://foo.test/following') + end + + describe '#bitmap' do + context 'when no policy is given' do + let(:json_policy) { nil } + + it 'returns zero' do + expect(subject.bitmap).to be_zero + end + end + + context 'with special public URI' do + let(:json_policy) do + { + 'manualApproval' => [public_uri], + } + end + + shared_examples 'setting the public bit' do + it 'sets the public bit' do + expect(subject.bitmap).to eq 0b10 + end + end + + context 'when public URI is given in full' do + let(:public_uri) { 'https://www.w3.org/ns/activitystreams#Public' } + + it_behaves_like 'setting the public bit' + end + + context 'when public URI is abbreviated using namespace' do + let(:public_uri) { 'as:Public' } + + it_behaves_like 'setting the public bit' + end + + context 'when public URI is abbreviated without namespace' do + let(:public_uri) { 'Public' } + + it_behaves_like 'setting the public bit' + end + end + + context 'when mixing array and scalar values' do + let(:json_policy) do + { + 'automaticApproval' => 'https://foo.test', + 'manualApproval' => [ + 'https://foo.test/followers', + 'https://foo.test/following', + ], + } + end + + it 'sets the correct flags' do + expect(subject.bitmap).to eq 0b100000000000000001100 + end + end + + context 'when including individual actor URIs' do + let(:json_policy) do + { + 'automaticApproval' => ['https://example.com/actor', 'https://masto.example.com/@user'], + 'manualApproval' => ['https://masto.example.com/@other'], + } + end + + it 'sets the unsupported bit' do + expect(subject.bitmap).to eq 0b10000000000000001 + end + end + + context "when giving the affected actor's URI in addition to other supported URIs" do + let(:json_policy) do + { + 'manualApproval' => [ + 'https://foo.test/followers', + 'https://foo.test/following', + 'https://foo.test', + ], + } + end + + it 'is being ignored' do + expect(subject.bitmap).to eq 0b1100 + end + end + end +end diff --git a/spec/lib/activitypub/parser/status_parser_spec.rb b/spec/lib/activitypub/parser/status_parser_spec.rb index b251b63f43cfb5..3084f3ffd611e9 100644 --- a/spec/lib/activitypub/parser/status_parser_spec.rb +++ b/spec/lib/activitypub/parser/status_parser_spec.rb @@ -148,7 +148,7 @@ end it 'returns a policy not allowing anyone to quote' do - expect(subject).to eq(Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16) + expect(subject).to eq(InteractionPolicy::POLICY_FLAGS[:public] << 16) end end @@ -174,7 +174,7 @@ end it 'returns a policy allowing everyone including followers' do - expect(subject).to eq Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] | (Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16) + expect(subject).to eq InteractionPolicy::POLICY_FLAGS[:public] | (InteractionPolicy::POLICY_FLAGS[:followers] << 16) end end end diff --git a/spec/lib/annual_report/time_series_spec.rb b/spec/lib/annual_report/time_series_spec.rb index 219d6c08347a28..046ac5b20257e0 100644 --- a/spec/lib/annual_report/time_series_spec.rb +++ b/spec/lib/annual_report/time_series_spec.rb @@ -13,7 +13,7 @@ expect(subject.generate) .to include( time_series: match( - include(followers: 0, following: 0, month: 1, statuses: 0) + include(followers: 0, month: 12, statuses: 0) ) ) end @@ -37,7 +37,7 @@ expect(subject.generate) .to include( time_series: match( - include(followers: 1, following: 1, month: 1, statuses: 1) + include(followers: 1, month: 12, statuses: 1) ) ) end diff --git a/spec/lib/annual_report/top_statuses_spec.rb b/spec/lib/annual_report/top_statuses_spec.rb index af29df1f651a0b..24fa2139ce25d2 100644 --- a/spec/lib/annual_report/top_statuses_spec.rb +++ b/spec/lib/annual_report/top_statuses_spec.rb @@ -40,8 +40,8 @@ .to include( top_statuses: include( by_reblogs: reblogged_status.id.to_s, - by_favourites: favourited_status.id.to_s, - by_replies: replied_status.id.to_s + by_favourites: nil, + by_replies: nil ) ) end diff --git a/spec/lib/interaction_policy_spec.rb b/spec/lib/interaction_policy_spec.rb new file mode 100644 index 00000000000000..2382c74349a45c --- /dev/null +++ b/spec/lib/interaction_policy_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe InteractionPolicy do + subject { described_class.new(bitmap) } + + let(:bitmap) { (0b0101 << 16) | 0b0010 } + + describe described_class::SubPolicy do + subject { InteractionPolicy.new(bitmap) } + + describe '#as_keys' do + it 'returns the expected values' do + expect(subject.automatic.as_keys).to eq ['unsupported_policy', 'followers'] + expect(subject.manual.as_keys).to eq ['public'] + end + end + + describe '#public?' do + it 'returns the expected values' do + expect(subject.automatic.public?).to be false + expect(subject.manual.public?).to be true + end + end + + describe '#unsupported_policy?' do + it 'returns the expected values' do + expect(subject.automatic.unsupported_policy?).to be true + expect(subject.manual.unsupported_policy?).to be false + end + end + + describe '#followers?' do + it 'returns the expected values' do + expect(subject.automatic.followers?).to be true + expect(subject.manual.followers?).to be false + end + end + end +end diff --git a/spec/lib/status_cache_hydrator_spec.rb b/spec/lib/status_cache_hydrator_spec.rb index 8ad4e5614ef868..a6fea36397a0ca 100644 --- a/spec/lib/status_cache_hydrator_spec.rb +++ b/spec/lib/status_cache_hydrator_spec.rb @@ -29,7 +29,7 @@ end context 'when handling a status with a quote policy' do - let(:status) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16) } + let(:status) { Fabricate(:status, quote_approval_policy: InteractionPolicy::POLICY_FLAGS[:followers] << 16) } before do account.follow!(status.account) diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index e3c722e8cba8b2..3a7648d1f791c9 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -967,4 +967,37 @@ def fields_empty_name expect(subject.reload.followers_count).to eq 15 end end + + describe '#featureable?' do + subject { Fabricate.build(:account, domain: (local ? nil : 'example.com'), discoverable:) } + + context 'when account is local' do + let(:local) { true } + + context 'when account is discoverable' do + let(:discoverable) { true } + + it 'returns `true`' do + expect(subject.featureable?).to be true + end + end + + context 'when account is not discoverable' do + let(:discoverable) { false } + + it 'returns `false`' do + expect(subject.featureable?).to be false + end + end + end + + context 'when account is remote' do + let(:local) { false } + let(:discoverable) { true } + + it 'returns `false`' do + expect(subject.featureable?).to be false + end + end + end end diff --git a/spec/models/collection_item_spec.rb b/spec/models/collection_item_spec.rb index 39464b7a34013a..3592c75e66789a 100644 --- a/spec/models/collection_item_spec.rb +++ b/spec/models/collection_item_spec.rb @@ -38,4 +38,23 @@ it { is_expected.to validate_presence_of(:object_uri) } end end + + describe 'Creation' do + let(:collection) { Fabricate(:collection) } + let(:other_collection) { Fabricate(:collection) } + let(:account) { Fabricate(:account) } + let(:other_account) { Fabricate(:account) } + + it 'automatically sets the `position` if absent' do + first_item = collection.collection_items.create(account:) + second_item = collection.collection_items.create(account: other_account) + unrelated_item = other_collection.collection_items.create(account:) + custom_item = other_collection.collection_items.create(account: other_account, position: 7) + + expect(first_item.position).to eq 1 + expect(second_item.position).to eq 2 + expect(unrelated_item.position).to eq 1 + expect(custom_item.position).to eq 7 + end + end end diff --git a/spec/models/concerns/account/finder_concern_spec.rb b/spec/models/concerns/account/finder_concern_spec.rb index b3fae56dfc51e8..18d5c204754c35 100644 --- a/spec/models/concerns/account/finder_concern_spec.rb +++ b/spec/models/concerns/account/finder_concern_spec.rb @@ -3,6 +3,37 @@ require 'rails_helper' RSpec.describe Account::FinderConcern do + describe '.representative' do + context 'with an instance actor using an invalid legacy username' do + let(:legacy_value) { 'localhost:3000' } + + before { Account.find(Account::INSTANCE_ACTOR_ID).update_attribute(:username, legacy_value) } + + it 'updates the username to the new value' do + expect { Account.representative } + .to change { Account.find(Account::INSTANCE_ACTOR_ID).username }.from(legacy_value).to('mastodon.internal') + end + end + + context 'without an instance actor' do + before { Account.find(Account::INSTANCE_ACTOR_ID).destroy! } + + it 'creates an instance actor' do + expect { Account.representative } + .to change(Account.where(id: Account::INSTANCE_ACTOR_ID), :count).from(0).to(1) + end + end + + context 'with a correctly loaded instance actor' do + let(:instance_actor) { Account.find(Account::INSTANCE_ACTOR_ID) } + + it 'returns the instance actor record' do + expect(Account.representative) + .to eq(instance_actor) + end + end + end + describe 'local finders' do let!(:account) { Fabricate(:account, username: 'Alice') } diff --git a/spec/models/concerns/account/interaction_policy_concern_spec.rb b/spec/models/concerns/account/interaction_policy_concern_spec.rb new file mode 100644 index 00000000000000..586a33b77f4ed4 --- /dev/null +++ b/spec/models/concerns/account/interaction_policy_concern_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Account::InteractionPolicyConcern do + describe '#feature_policy_as_keys' do + context 'when account is local' do + context 'when account is discoverable' do + let(:account) { Fabricate(:account) } + + it 'returns public for automtatic and nothing for manual' do + expect(account.feature_policy_as_keys(:automatic)).to eq [:public] + expect(account.feature_policy_as_keys(:manual)).to eq [] + end + end + + context 'when account is not discoverable' do + let(:account) { Fabricate(:account, discoverable: false) } + + it 'returns empty arrays for both inputs' do + expect(account.feature_policy_as_keys(:automatic)).to eq [] + expect(account.feature_policy_as_keys(:manual)).to eq [] + end + end + end + + context 'when account is remote' do + let(:account) { Fabricate(:account, domain: 'example.com', feature_approval_policy: (0b0101 << 16) | 0b0010) } + + it 'returns the expected values' do + expect(account.feature_policy_as_keys(:automatic)).to eq ['unsupported_policy', 'followers'] + expect(account.feature_policy_as_keys(:manual)).to eq ['public'] + end + end + end + + describe '#feature_policy_for_account' do + context 'when account is remote' do + let(:account) { Fabricate(:account, domain: 'example.com', feature_approval_policy:) } + let(:feature_approval_policy) { (0b0101 << 16) | 0b0010 } + let(:other_account) { Fabricate(:account) } + + context 'when no policy is available' do + let(:feature_approval_policy) { 0 } + + context 'when both accounts are the same' do + it 'returns :automatic' do + expect(account.feature_policy_for_account(account)).to eq :automatic + end + end + + context 'with two different accounts' do + it 'returns :missing' do + expect(account.feature_policy_for_account(other_account)).to eq :missing + end + end + end + + context 'when the other account is not following the account' do + it 'returns :manual because of the public entry in the manual policy' do + expect(account.feature_policy_for_account(other_account)).to eq :manual + end + end + + context 'when the other account is following the account' do + before do + other_account.follow!(account) + end + + it 'returns :automatic because of the followers entry in the automatic policy' do + expect(account.feature_policy_for_account(other_account)).to eq :automatic + end + end + + context 'when the account falls into the unknown bucket' do + let(:feature_approval_policy) { (0b0001 << 16) | 0b0100 } + + it 'returns :automatic because of the followers entry in the automatic policy' do + expect(account.feature_policy_for_account(other_account)).to eq :unknown + end + end + end + end +end diff --git a/spec/models/concerns/account/interactions_spec.rb b/spec/models/concerns/account/interactions_spec.rb index f882acf980616f..982eaf3aa37338 100644 --- a/spec/models/concerns/account/interactions_spec.rb +++ b/spec/models/concerns/account/interactions_spec.rb @@ -304,9 +304,24 @@ subject { account.following?(target_account) } context 'when following target_account' do - it 'returns true' do + before do account.active_relationships.create(target_account: target_account) - expect(subject).to be true + end + + it 'returns true' do + result = nil + expect { result = subject }.to execute_queries + expect(result).to be true + end + + context 'when relations are preloaded' do + it 'does not query the database to get the result' do + account.preload_relations!([target_account.id]) + + result = nil + expect { result = subject }.to_not execute_queries + expect(result).to be true + end end end @@ -375,9 +390,26 @@ subject { account.blocking?(target_account) } context 'when blocking target_account' do - it 'returns true' do + before do account.block_relationships.create(target_account: target_account) - expect(subject).to be true + end + + it 'returns true' do + result = nil + expect { result = subject }.to execute_queries + + expect(result).to be true + end + + context 'when relations are preloaded' do + it 'does not query the database to get the result' do + account.preload_relations!([target_account.id]) + + result = nil + expect { result = subject }.to_not execute_queries + + expect(result).to be true + end end end @@ -388,16 +420,65 @@ end end + describe '#blocked_by?' do + subject { account.blocked_by?(target_account) } + + context 'when blocked by target_account' do + before do + target_account.block_relationships.create(target_account: account) + end + + it 'returns true' do + result = nil + expect { result = subject }.to execute_queries + + expect(result).to be true + end + + context 'when relations are preloaded' do + it 'does not query the database to get the result' do + account.preload_relations!([target_account.id]) + + result = nil + expect { result = subject }.to_not execute_queries + + expect(result).to be true + end + end + end + + context 'when not blocked by target_account' do + it 'returns false' do + expect(subject).to be false + end + end + end + describe '#domain_blocking?' do subject { account.domain_blocking?(domain) } let(:domain) { 'example.com' } context 'when blocking the domain' do - it 'returns true' do + before do account_domain_block = Fabricate(:account_domain_block, domain: domain) account.domain_blocks << account_domain_block - expect(subject).to be true + end + + it 'returns true' do + result = nil + expect { result = subject }.to execute_queries + expect(result).to be true + end + + context 'when relations are preloaded' do + it 'does not query the database to get the result' do + account.preload_relations!([], [domain]) + + result = nil + expect { result = subject }.to_not execute_queries + expect(result).to be true + end end end @@ -412,10 +493,25 @@ subject { account.muting?(target_account) } context 'when muting target_account' do - it 'returns true' do + before do mute = Fabricate(:mute, account: account, target_account: target_account) account.mute_relationships << mute - expect(subject).to be true + end + + it 'returns true' do + result = nil + expect { result = subject }.to execute_queries + expect(result).to be true + end + + context 'when relations are preloaded' do + it 'does not query the database to get the result' do + account.preload_relations!([target_account.id]) + + result = nil + expect { result = subject }.to_not execute_queries + expect(result).to be true + end end end diff --git a/spec/models/concerns/status/threading_concern_spec.rb b/spec/models/concerns/status/threading_concern_spec.rb index 5b320c928d88bc..6e0b2afc666ba2 100644 --- a/spec/models/concerns/status/threading_concern_spec.rb +++ b/spec/models/concerns/status/threading_concern_spec.rb @@ -131,7 +131,7 @@ end describe '#readable_references' do - subject { status.readable_references(account).pluck(:id) } + subject { status.readable_references(10, account).pluck(:id) } let(:visibility) { :public } let(:alice) { Fabricate(:account) } diff --git a/spec/models/domain_allow_spec.rb b/spec/models/domain_allow_spec.rb index 0c69aaff8dc84d..ac0c113d3a9ca3 100644 --- a/spec/models/domain_allow_spec.rb +++ b/spec/models/domain_allow_spec.rb @@ -27,4 +27,27 @@ it { is_expected.to contain_exactly(allowed_domain.domain, other_allowed_domain.domain) } end end + + describe '.rule_for' do + subject { described_class.rule_for(domain) } + + let(:domain) { 'host.example' } + + context 'with no records' do + it { is_expected.to be_nil } + end + + context 'with matching record' do + let!(:domain_allow) { Fabricate :domain_allow, domain: } + + it { is_expected.to eq(domain_allow) } + end + + context 'when called with non normalized string' do + let!(:domain_allow) { Fabricate :domain_allow, domain: } + let(:domain) { ' HOST.example/' } + + it { is_expected.to eq(domain_allow) } + end + end end diff --git a/spec/models/domain_block_spec.rb b/spec/models/domain_block_spec.rb index 14f904ea7fa6a5..e7f463c8f569a4 100644 --- a/spec/models/domain_block_spec.rb +++ b/spec/models/domain_block_spec.rb @@ -35,6 +35,17 @@ expect(described_class.rule_for('example.com')).to eq block end + it 'returns most specific rule matching a blocked domain' do + _block = Fabricate(:domain_block, domain: 'example.com') + blog_block = Fabricate(:domain_block, domain: 'blog.example.com') + expect(described_class.rule_for('host.blog.example.com')).to eq blog_block + end + + it 'returns rule matching a blocked domain when string needs normalization' do + block = Fabricate(:domain_block, domain: 'example.com') + expect(described_class.rule_for(' example.com/')).to eq block + end + it 'returns a rule matching a subdomain of a blocked domain' do block = Fabricate(:domain_block, domain: 'example.com') expect(described_class.rule_for('sub.example.com')).to eq block diff --git a/spec/models/email_domain_block_spec.rb b/spec/models/email_domain_block_spec.rb index 5874c5e53c4595..c3662b2d6cd83f 100644 --- a/spec/models/email_domain_block_spec.rb +++ b/spec/models/email_domain_block_spec.rb @@ -4,6 +4,8 @@ RSpec.describe EmailDomainBlock do describe 'block?' do + subject { described_class.block?(input) } + let(:input) { nil } context 'when given an e-mail address' do @@ -14,12 +16,12 @@ it 'returns true if the domain is blocked' do Fabricate(:email_domain_block, domain: 'example.com') - expect(described_class.block?(input)).to be true + expect(subject).to be true end it 'returns false if the domain is not blocked' do Fabricate(:email_domain_block, domain: 'other-example.com') - expect(described_class.block?(input)).to be false + expect(subject).to be false end end @@ -28,7 +30,7 @@ it 'returns true if it is a subdomain of a blocked domain' do Fabricate(:email_domain_block, domain: 'example.com') - expect(described_class.block?(input)).to be true + expect(subject).to be true end end end @@ -38,8 +40,40 @@ it 'returns true if the domain is blocked' do Fabricate(:email_domain_block, domain: 'mail.foo.com') - expect(described_class.block?(input)).to be true + expect(subject).to be true end end + + context 'when given nil' do + it { is_expected.to be false } + end + + context 'when given empty string' do + let(:input) { '' } + + it { is_expected.to be true } + end + end + + describe '.requires_approval?' do + subject { described_class.requires_approval?(input) } + + let(:input) { nil } + + context 'with a matching block requiring approval' do + before { Fabricate :email_domain_block, domain: input, allow_with_approval: true } + + let(:input) { 'host.example' } + + it { is_expected.to be true } + end + + context 'with a matching block not requiring approval' do + before { Fabricate :email_domain_block, domain: input, allow_with_approval: false } + + let(:input) { 'host.example' } + + it { is_expected.to be false } + end end end diff --git a/spec/models/form/admin_settings_spec.rb b/spec/models/form/admin_settings_spec.rb index 899d56703a9a63..e734734e000376 100644 --- a/spec/models/form/admin_settings_spec.rb +++ b/spec/models/form/admin_settings_spec.rb @@ -53,4 +53,8 @@ end end end + + describe '#persisted?' do + it { is_expected.to be_persisted } + end end diff --git a/spec/models/preview_card_provider_spec.rb b/spec/models/preview_card_provider_spec.rb index 561c93d0b2571d..cd3283faa3f0aa 100644 --- a/spec/models/preview_card_provider_spec.rb +++ b/spec/models/preview_card_provider_spec.rb @@ -25,4 +25,36 @@ end end end + + describe '.matching_domain' do + subject { described_class.matching_domain(domain) } + + let(:domain) { 'host.example' } + + context 'without matching domains' do + it { is_expected.to be_nil } + end + + context 'with exact matching domain' do + let!(:preview_card_provider) { Fabricate :preview_card_provider, domain: 'host.example' } + + it { is_expected.to eq(preview_card_provider) } + end + + context 'with matching domain segment' do + let!(:preview_card_provider) { Fabricate :preview_card_provider, domain: 'host.example' } + let(:domain) { 'www.blog.host.example' } + + it { is_expected.to eq(preview_card_provider) } + end + + context 'with multiple matching records' do + let!(:preview_card_provider_more) { Fabricate :preview_card_provider, domain: 'blog.host.example' } + let(:domain) { 'www.blog.host.example' } + + before { Fabricate :preview_card_provider, domain: 'host.example' } + + it { is_expected.to eq(preview_card_provider_more) } + end + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 7088266b34eb38..4ea2e6a79ca649 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -39,6 +39,15 @@ end it { is_expected.to allow_value('admin@localhost').for(:email) } + + context 'when registration form time is present' do + subject { Fabricate.build :user } + + before { stub_const 'RegistrationFormTimeValidator::REGISTRATION_FORM_MIN_TIME', 3.seconds } + + it { is_expected.to allow_value(10.seconds.ago).for(:registration_form_time) } + it { is_expected.to_not allow_value(1.second.ago).for(:registration_form_time).against(:base) } + end end describe 'Normalizations' do diff --git a/spec/policies/account_policy_spec.rb b/spec/policies/account_policy_spec.rb index 8b2edb15b0196d..f877bded252c70 100644 --- a/spec/policies/account_policy_spec.rb +++ b/spec/policies/account_policy_spec.rb @@ -156,4 +156,36 @@ end end end + + permissions :feature? do + context 'when account is featureable?' do + it 'permits' do + expect(subject).to permit(alice, john) + end + end + + context 'when account is not featureable' do + before { allow(alice).to receive(:featureable?).and_return(false) } + + it 'denies' do + expect(subject).to_not permit(john, alice) + end + end + + context 'when account is blocked' do + before { alice.block!(john) } + + it 'denies' do + expect(subject).to_not permit(alice, john) + end + end + + context 'when account is blocking' do + before { john.block!(alice) } + + it 'denies' do + expect(subject).to_not permit(alice, john) + end + end + end end diff --git a/spec/policies/status_policy_spec.rb b/spec/policies/status_policy_spec.rb index 7b0b95b53df06d..e7e19b1598e3b1 100644 --- a/spec/policies/status_policy_spec.rb +++ b/spec/policies/status_policy_spec.rb @@ -247,19 +247,19 @@ end it 'grants access when public and policy allows everyone' do - status.quote_approval_policy = Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] + status.quote_approval_policy = InteractionPolicy::POLICY_FLAGS[:public] viewer = Fabricate(:account) expect(subject).to permit(viewer, status) end it 'denies access when public and policy allows followers but viewer is not one' do - status.quote_approval_policy = Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] + status.quote_approval_policy = InteractionPolicy::POLICY_FLAGS[:followers] viewer = Fabricate(:account) expect(subject).to_not permit(viewer, status) end it 'grants access when public and policy allows followers and viewer is one' do - status.quote_approval_policy = Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] + status.quote_approval_policy = InteractionPolicy::POLICY_FLAGS[:followers] viewer = Fabricate(:account) viewer.follow!(status.account) expect(subject).to permit(viewer, status) diff --git a/spec/requests/api/v1/accounts_spec.rb b/spec/requests/api/v1/accounts_spec.rb index 675d0a4450cb76..75f30f5431af47 100644 --- a/spec/requests/api/v1/accounts_spec.rb +++ b/spec/requests/api/v1/accounts_spec.rb @@ -96,6 +96,28 @@ end end + context 'when missing username value' do + subject do + post '/api/v1/accounts', headers: headers, params: { password: '12345678', email: 'hello@world.tld', agreement: 'true' } + end + + it 'returns http unprocessable entity with username error message' do + expect { subject } + .to not_change(User, :count) + .and not_change(Account, :count) + + expect(response) + .to have_http_status(422) + expect(response.media_type) + .to eq('application/json') + expect(response.parsed_body) + .to include( + error: /Validation failed/, + details: include(username: contain_exactly(include(error: 'ERR_BLANK', description: /can't be blank/))) + ) + end + end + context 'when age verification is enabled' do before do Setting.min_age = 16 diff --git a/spec/requests/api/v1/annual_reports_spec.rb b/spec/requests/api/v1/annual_reports_spec.rb index 482e91736c91a4..e79309145e3a54 100644 --- a/spec/requests/api/v1/annual_reports_spec.rb +++ b/spec/requests/api/v1/annual_reports_spec.rb @@ -85,6 +85,10 @@ end context 'when the feature is not enabled' do + before do + Setting.wrapstodon = false + end + it 'returns http success and ineligible status' do get '/api/v1/annual_reports/2025/state', headers: headers @@ -99,7 +103,7 @@ end end - context 'when the feature is enabled and time is within window', feature: :wrapstodon do + context 'when the feature is enabled and time is within window' do before do travel_to Time.utc(2025, 12, 20) @@ -121,7 +125,7 @@ end end - context 'when the feature is enabled but we are out of the time window', feature: :wrapstodon do + context 'when the feature is enabled but we are out of the time window' do before do travel_to Time.utc(2025, 6, 20) @@ -168,7 +172,7 @@ context 'with correct scope' do let(:scopes) { 'write:accounts' } - context 'when the feature is enabled and time is within window', feature: :wrapstodon do + context 'when the feature is enabled and time is within window' do before do travel_to Time.utc(2025, 12, 20) diff --git a/spec/requests/api/v1/apps/credentials_spec.rb b/spec/requests/api/v1/apps/credentials_spec.rb index 8c0292d8c3e92b..09363362d62b11 100644 --- a/spec/requests/api/v1/apps/credentials_spec.rb +++ b/spec/requests/api/v1/apps/credentials_spec.rb @@ -75,6 +75,33 @@ end end + context 'with client credentials' do + let(:application) { Fabricate(:application, scopes: 'read admin:write') } + let(:token) { Fabricate(:client_credentials_token, application: application, scopes: 'read admin:write') } + let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } + + it 'returns http success and returns app information' do + subject + + expect(response).to have_http_status(200) + expect(response.content_type) + .to start_with('application/json') + + expect(response.parsed_body).to match( + a_hash_including( + id: token.application.id.to_s, + name: token.application.name, + website: token.application.website, + scopes: token.application.scopes.map(&:to_s), + redirect_uris: token.application.redirect_uris, + # Deprecated properties as of 4.3: + redirect_uri: token.application.redirect_uri.split.first, + vapid_key: Rails.configuration.x.vapid.public_key + ) + ) + end + end + context 'without an oauth token' do let(:headers) { {} } diff --git a/spec/requests/api/v1/statuses/interaction_policies_spec.rb b/spec/requests/api/v1/statuses/interaction_policies_spec.rb index 321a68cd2519a0..aa5819cdd70518 100644 --- a/spec/requests/api/v1/statuses/interaction_policies_spec.rb +++ b/spec/requests/api/v1/statuses/interaction_policies_spec.rb @@ -46,7 +46,7 @@ context 'when changing the interaction policy' do it 'changes the interaction policy, returns the updated status, and schedules distribution jobs' do expect { subject } - .to change { status.reload.quote_approval_policy }.to(Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16) + .to change { status.reload.quote_approval_policy }.to(InteractionPolicy::POLICY_FLAGS[:followers] << 16) expect(response).to have_http_status(200) expect(response.content_type) diff --git a/spec/requests/api/v1/statuses_spec.rb b/spec/requests/api/v1/statuses_spec.rb index ae46ebad99c110..af82e3ebcbc477 100644 --- a/spec/requests/api/v1/statuses_spec.rb +++ b/spec/requests/api/v1/statuses_spec.rb @@ -353,7 +353,7 @@ end context 'with a quote to a non-mentioned user in a Private Mention' do - let!(:quoted_status) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16) } + let!(:quoted_status) { Fabricate(:status, quote_approval_policy: InteractionPolicy::POLICY_FLAGS[:public] << 16) } let(:params) do { status: 'Hello, this is a quote', @@ -372,7 +372,7 @@ end context 'with a quote to a mentioned user in a Private Mention' do - let!(:quoted_status) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16) } + let!(:quoted_status) { Fabricate(:status, quote_approval_policy: InteractionPolicy::POLICY_FLAGS[:public] << 16) } let(:params) do { status: "Hello @#{quoted_status.account.acct}, this is a quote", @@ -394,7 +394,7 @@ end context 'with a quote of a reblog' do - let(:quoted_status) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16) } + let(:quoted_status) { Fabricate(:status, quote_approval_policy: InteractionPolicy::POLICY_FLAGS[:public] << 16) } let(:reblog) { Fabricate(:status, reblog: quoted_status) } let(:params) do { @@ -590,7 +590,7 @@ it 'updates the status', :aggregate_failures do expect { subject } - .to change { status.reload.quote_approval_policy }.to(Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16) + .to change { status.reload.quote_approval_policy }.to(InteractionPolicy::POLICY_FLAGS[:public] << 16) expect(response).to have_http_status(200) expect(response.content_type) diff --git a/spec/requests/api/v1_alpha/collection_items_spec.rb b/spec/requests/api/v1_alpha/collection_items_spec.rb new file mode 100644 index 00000000000000..5c44a7edf80e80 --- /dev/null +++ b/spec/requests/api/v1_alpha/collection_items_spec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Api::V1Alpha::CollectionItems', feature: :collections do + include_context 'with API authentication', oauth_scopes: 'read:collections write:collections' + + describe 'POST /api/v1_alpha/collections/:collection_id/items' do + subject do + post "/api/v1_alpha/collections/#{collection.id}/items", headers: headers, params: params + end + + let(:collection) { Fabricate(:collection, account: user.account) } + let(:params) { {} } + + it_behaves_like 'forbidden for wrong scope', 'read' + + context 'when user is owner of the collection' do + context 'with valid params' do + let(:other_account) { Fabricate(:account) } + let(:params) { { account_id: other_account.id } } + + it 'creates a collection item and returns http success' do + expect do + subject + end.to change(collection.collection_items, :count).by(1) + + expect(response).to have_http_status(200) + end + end + + context 'with invalid params' do + it 'returns http unprocessable content' do + expect do + subject + end.to_not change(CollectionItem, :count) + + expect(response).to have_http_status(422) + end + end + end + + context 'when user is not the owner of the collection' do + let(:collection) { Fabricate(:collection) } + let(:other_account) { Fabricate(:account) } + let(:params) { { account_id: other_account.id } } + + it 'returns http forbidden' do + subject + + expect(response).to have_http_status(403) + end + end + end + + describe 'DELETE /api/v1_alpha/collections/:collection_id/items/:id' do + subject do + delete "/api/v1_alpha/collections/#{collection.id}/items/#{item.id}", headers: headers + end + + let(:collection) { Fabricate(:collection, account: user.account) } + let(:item) { Fabricate(:collection_item, collection:) } + + it_behaves_like 'forbidden for wrong scope', 'read' + + context 'when user is owner of the collection' do + context 'when item belongs to collection' do + it 'deletes the collection item and returns http success' do + item # Make sure this exists before calling the API + + expect do + subject + end.to change(collection.collection_items, :count).by(-1) + + expect(response).to have_http_status(200) + end + end + + context 'when item does not belong to to collection' do + let(:item) { Fabricate(:collection_item) } + + it 'returns http not found' do + item # Make sure this exists before calling the API + + expect do + subject + end.to_not change(CollectionItem, :count) + + expect(response).to have_http_status(404) + end + end + end + + context 'when user is not the owner of the collection' do + let(:collection) { Fabricate(:collection) } + + it 'returns http forbidden' do + subject + + expect(response).to have_http_status(403) + end + end + end +end diff --git a/spec/requests/api/v2/instance_spec.rb b/spec/requests/api/v2/instance_spec.rb index 50798612e83b22..39e7105f095dc7 100644 --- a/spec/requests/api/v2/instance_spec.rb +++ b/spec/requests/api/v2/instance_spec.rb @@ -42,7 +42,7 @@ end end - context 'when wrapstodon is enabled', feature: :wrapstodon do + context 'when wrapstodon is enabled' do before do travel_to Time.utc(2025, 12, 20) end diff --git a/spec/requests/wrapstodon_spec.rb b/spec/requests/wrapstodon_spec.rb new file mode 100644 index 00000000000000..62bd4dacd91759 --- /dev/null +++ b/spec/requests/wrapstodon_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Wrapstodon' do + let(:generated_annual_report) { AnnualReport.new(user.account, Time.current.year).generate } + let(:user) { Fabricate :user } + + describe 'GET /@:account_username/wrapstodon/:year/:share_key' do + context 'when share_key is invalid' do + it 'returns not found' do + get public_wrapstodon_path(account_username: user.account.username, year: generated_annual_report.year, share_key: 'sharks') + + expect(response) + .to have_http_status(404) + end + end + end +end diff --git a/spec/serializers/activitypub/actor_serializer_spec.rb b/spec/serializers/activitypub/actor_serializer_spec.rb index ad2445595375ac..734da6673c0d3d 100644 --- a/spec/serializers/activitypub/actor_serializer_spec.rb +++ b/spec/serializers/activitypub/actor_serializer_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe ActivityPub::ActorSerializer do - subject { serialized_record_json(record, described_class) } + subject { serialized_record_json(record, described_class, adapter: ActivityPub::Adapter) } describe '#type' do context 'with the instance actor' do @@ -36,4 +36,39 @@ it { is_expected.to include('type' => 'Person') } end end + + describe '#interactionPolicy' do + let(:record) { Fabricate(:account) } + + # TODO: Remove when feature flag is removed + context 'when collections feature is disabled?' do + it 'is not present' do + expect(subject).to_not have_key('interactionPolicy') + end + end + + context 'when collections feature is enabled', feature: :collections do + context 'when actor is discoverable' do + it 'includes an automatic policy allowing everyone' do + expect(subject).to include('interactionPolicy' => { + 'canFeature' => { + 'automaticApproval' => ['https://www.w3.org/ns/activitystreams#Public'], + }, + }) + end + end + + context 'when actor is not discoverable' do + let(:record) { Fabricate(:account, discoverable: false) } + + it 'includes an automatic policy limited to the actor itself' do + expect(subject).to include('interactionPolicy' => { + 'canFeature' => { + 'automaticApproval' => [ActivityPub::TagManager.instance.uri_for(record)], + }, + }) + end + end + end + end end diff --git a/spec/serializers/activitypub/note_serializer_spec.rb b/spec/serializers/activitypub/note_serializer_spec.rb index 42058b30b3a75b..a987ae5a349f87 100644 --- a/spec/serializers/activitypub/note_serializer_spec.rb +++ b/spec/serializers/activitypub/note_serializer_spec.rb @@ -205,7 +205,7 @@ def reply_items end context 'with a quote policy' do - let(:parent) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16) } + let(:parent) { Fabricate(:status, quote_approval_policy: InteractionPolicy::POLICY_FLAGS[:followers] << 16) } it 'has the expected shape' do expect(subject).to include({ diff --git a/spec/serializers/rest/account_serializer_spec.rb b/spec/serializers/rest/account_serializer_spec.rb index 5fd4f8d706d394..998de6b0fb990a 100644 --- a/spec/serializers/rest/account_serializer_spec.rb +++ b/spec/serializers/rest/account_serializer_spec.rb @@ -3,12 +3,18 @@ require 'rails_helper' RSpec.describe REST::AccountSerializer do - subject { serialized_record_json(account, described_class) } + subject do + serialized_record_json(account, described_class, options: { + scope: current_user, + scope_name: :current_user, + }) + end let(:default_datetime) { DateTime.new(2024, 11, 28, 16, 20, 0) } let(:role) { Fabricate(:user_role, name: 'Role', highlighted: true) } let(:user) { Fabricate(:user, role: role) } let(:account) { user.account } + let(:current_user) { Fabricate(:user) } context 'when the account is suspended' do before do @@ -68,4 +74,51 @@ expect(subject['last_status_at']).to eq('2024-11-28') end end + + describe '#feature_approval' do + # TODO: Remove when feature flag is removed + context 'when collections feature is disabled' do + it 'does not include the approval policy' do + expect(subject).to_not have_key('feature_approval') + end + end + + context 'when collections feature is enabled', feature: :collections do + context 'when account is local' do + context 'when account is discoverable' do + it 'includes a policy that allows featuring' do + expect(subject['feature_approval']).to include({ + 'automatic' => ['public'], + 'manual' => [], + 'current_user' => 'automatic', + }) + end + end + + context 'when account is not discoverable' do + let(:account) { Fabricate(:account, discoverable: false) } + + it 'includes a policy that disallows featuring' do + expect(subject['feature_approval']).to include({ + 'automatic' => [], + 'manual' => [], + 'current_user' => 'denied', + }) + end + end + end + + context 'when account is remote' do + let(:account) { Fabricate(:account, domain: 'example.com', feature_approval_policy: 0b11000000000000000010) } + + it 'includes the matching policy' do + expect(subject['feature_approval']).to include({ + 'automatic' => ['followers', 'following'], + 'manual' => ['public'], + 'current_user' => 'manual', + }) + end + end + end + end end diff --git a/spec/serializers/rest/base_collection_serializer_spec.rb b/spec/serializers/rest/base_collection_serializer_spec.rb index c988a247e7fb36..5ac6bc615d058c 100644 --- a/spec/serializers/rest/base_collection_serializer_spec.rb +++ b/spec/serializers/rest/base_collection_serializer_spec.rb @@ -38,20 +38,4 @@ 'updated_at' => match_api_datetime_format ) end - - describe 'Counting items' do - before do - Fabricate.times(2, :collection_item, collection:) - end - - it 'can count items on demand' do - expect(subject['item_count']).to eq 2 - end - - it 'can use precalculated counts' do - collection.define_singleton_method :item_count, -> { 8 } - - expect(subject['item_count']).to eq 8 - end - end end diff --git a/spec/serializers/rest/collection_item_serializer_spec.rb b/spec/serializers/rest/collection_item_serializer_spec.rb index bcb7458c4de259..b12553ec034b8c 100644 --- a/spec/serializers/rest/collection_item_serializer_spec.rb +++ b/spec/serializers/rest/collection_item_serializer_spec.rb @@ -7,6 +7,7 @@ let(:collection_item) do Fabricate(:collection_item, + id: 2342, state:, position: 4) end @@ -17,6 +18,7 @@ it 'includes the relevant attributes including the account' do expect(subject) .to include( + 'id' => '2342', 'account' => an_instance_of(Hash), 'state' => 'accepted', 'position' => 4 diff --git a/spec/serializers/rest/scheduled_status_serializer_spec.rb b/spec/serializers/rest/scheduled_status_serializer_spec.rb index 6fc2f2eca9cd67..9eb27035d3fd44 100644 --- a/spec/serializers/rest/scheduled_status_serializer_spec.rb +++ b/spec/serializers/rest/scheduled_status_serializer_spec.rb @@ -10,7 +10,7 @@ ) end - let(:scheduled_status) { Fabricate.build(:scheduled_status, scheduled_at: 4.minutes.from_now, params: { application_id: 123, quoted_status_id: 456, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16 }) } + let(:scheduled_status) { Fabricate.build(:scheduled_status, scheduled_at: 4.minutes.from_now, params: { application_id: 123, quoted_status_id: 456, quote_approval_policy: InteractionPolicy::POLICY_FLAGS[:public] << 16 }) } describe 'serialization' do it 'returns expected values and removes application_id from params' do diff --git a/spec/services/activitypub/fetch_remote_status_service_spec.rb b/spec/services/activitypub/fetch_remote_status_service_spec.rb index 07d05d762f102a..6afee5f25ef263 100644 --- a/spec/services/activitypub/fetch_remote_status_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_status_service_spec.rb @@ -330,7 +330,7 @@ end it 'updates status' do - expect(existing_status.reload.quote_approval_policy).to eq(Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16) + expect(existing_status.reload.quote_approval_policy).to eq(InteractionPolicy::POLICY_FLAGS[:public] << 16) end end end diff --git a/spec/services/activitypub/process_account_service_spec.rb b/spec/services/activitypub/process_account_service_spec.rb index 057ebe13313aa1..f090b357d14eae 100644 --- a/spec/services/activitypub/process_account_service_spec.rb +++ b/spec/services/activitypub/process_account_service_spec.rb @@ -667,6 +667,49 @@ end end + context 'with interaction policy' do + let(:payload) do + { + id: 'https://foo.test', + type: 'Actor', + inbox: 'https://foo.test/inbox', + followers: 'https://foo.test/followers', + following: 'https://foo.test/following', + interactionPolicy: { + canFeature: { + automaticApproval: 'https://foo.test', + manualApproval: [ + 'https://foo.test/followers', + 'https://foo.test/following', + ], + }, + }, + }.with_indifferent_access + end + + before do + stub_request(:get, %r{^https://foo\.test/follow}) + .to_return(status: 200, body: '', headers: {}) + end + + # TODO: Remove when feature flag is removed + context 'when collections feature is disabled' do + it 'does not set the interaction policy' do + account = subject.call('user1', 'foo.test', payload) + + expect(account.feature_approval_policy).to be_zero + end + end + + context 'when collections feature is enabled', feature: :collections do + it 'sets the interaction policy to the correct value' do + account = subject.call('user1', 'foo.test', payload) + + expect(account.feature_approval_policy).to eq 0b100000000000000001100 + end + end + end + private def create_some_remote_accounts diff --git a/spec/services/activitypub/process_status_update_service_spec.rb b/spec/services/activitypub/process_status_update_service_spec.rb index aa0c714aa68c83..58264cdef42b7d 100644 --- a/spec/services/activitypub/process_status_update_service_spec.rb +++ b/spec/services/activitypub/process_status_update_service_spec.rb @@ -921,7 +921,7 @@ context 'when an approved quote of a local post gets updated through an explicit update, removing text' do let(:quoted_account) { Fabricate(:account) } - let(:quoted_status) { Fabricate(:status, account: quoted_account, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16) } + let(:quoted_status) { Fabricate(:status, account: quoted_account, quote_approval_policy: InteractionPolicy::POLICY_FLAGS[:public] << 16) } let!(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, state: :accepted) } let(:approval_uri) { ActivityPub::TagManager.instance.approval_uri_for(quote) } @@ -957,7 +957,7 @@ context 'when an approved quote of a local post gets updated through an explicit update' do let(:quoted_account) { Fabricate(:account) } - let(:quoted_status) { Fabricate(:status, account: quoted_account, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16) } + let(:quoted_status) { Fabricate(:status, account: quoted_account, quote_approval_policy: InteractionPolicy::POLICY_FLAGS[:public] << 16) } let!(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, state: :accepted) } let(:approval_uri) { ActivityPub::TagManager.instance.approval_uri_for(quote) } diff --git a/spec/services/add_account_to_collection_service_spec.rb b/spec/services/add_account_to_collection_service_spec.rb new file mode 100644 index 00000000000000..98d88d0b8f93c5 --- /dev/null +++ b/spec/services/add_account_to_collection_service_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe AddAccountToCollectionService do + subject { described_class.new } + + let(:collection) { Fabricate.create(:collection) } + + describe '#call' do + context 'when given a featurable account' do + let(:account) { Fabricate(:account) } + + it 'creates a new CollectionItem in the `accepted` state' do + expect do + subject.call(collection, account) + end.to change(collection.collection_items, :count).by(1) + + new_item = collection.collection_items.last + expect(new_item.state).to eq 'accepted' + expect(new_item.account).to eq account + end + end + + context 'when given an account that is not featureable' do + let(:account) { Fabricate(:account, discoverable: false) } + + it 'raises an error' do + expect do + subject.call(collection, account) + end.to raise_error(Mastodon::NotPermittedError) + end + end + end +end diff --git a/spec/services/create_collection_service_spec.rb b/spec/services/create_collection_service_spec.rb index bf59e299b1fdc2..f88a366a6c2625 100644 --- a/spec/services/create_collection_service_spec.rb +++ b/spec/services/create_collection_service_spec.rb @@ -30,9 +30,10 @@ end context 'when given account ids' do - let(:account_ids) do - Fabricate.times(2, :account).map { |a| a.id.to_s } + let(:accounts) do + Fabricate.times(2, :account) end + let(:account_ids) { accounts.map { |a| a.id.to_s } } let(:params) do base_params.merge(account_ids:) end @@ -42,6 +43,18 @@ subject.call(params, author) end.to change(CollectionItem, :count).by(2) end + + context 'when one account may not be added' do + before do + accounts.last.update(discoverable: false) + end + + it 'raises an error' do + expect do + subject.call(params, author) + end.to raise_error(Mastodon::NotPermittedError) + end + end end context 'when given a tag' do diff --git a/spec/services/post_status_service_spec.rb b/spec/services/post_status_service_spec.rb index da293ca60381f2..a99edefb81cef3 100644 --- a/spec/services/post_status_service_spec.rb +++ b/spec/services/post_status_service_spec.rb @@ -210,9 +210,9 @@ end it 'creates a status with the quote approval policy set' do - status = create_status_with_options(quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16) + status = create_status_with_options(quote_approval_policy: InteractionPolicy::POLICY_FLAGS[:followers] << 16) - expect(status.quote_approval_policy).to eq(Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16) + expect(status.quote_approval_policy).to eq(InteractionPolicy::POLICY_FLAGS[:followers] << 16) end it 'processes mentions' do diff --git a/spec/services/process_mentions_service_spec.rb b/spec/services/process_mentions_service_spec.rb index 9add51523ebb27..cc0e7e4fd4333e 100644 --- a/spec/services/process_mentions_service_spec.rb +++ b/spec/services/process_mentions_service_spec.rb @@ -8,9 +8,9 @@ let(:account) { Fabricate(:account, username: 'alice') } context 'when mentions contain blocked accounts' do - let(:non_blocked_account) { Fabricate(:account) } - let(:individually_blocked_account) { Fabricate(:account) } - let(:domain_blocked_account) { Fabricate(:account, domain: 'evil.com') } + let!(:non_blocked_account) { Fabricate(:account) } + let!(:individually_blocked_account) { Fabricate(:account) } + let!(:domain_blocked_account) { Fabricate(:account, domain: 'evil.com', protocol: :activitypub) } let(:status) { Fabricate(:status, account: account, text: "Hello @#{non_blocked_account.acct} @#{individually_blocked_account.acct} @#{domain_blocked_account.acct}", visibility: :public) } before do diff --git a/spec/support/matchers/sql_queries.rb b/spec/support/matchers/sql_queries.rb new file mode 100644 index 00000000000000..c37eefb06dbbaa --- /dev/null +++ b/spec/support/matchers/sql_queries.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'active_record/testing/query_assertions' + +# Implement something similar to Rails' built-in assertion. +# Can be removed once https://github.com/rspec/rspec-rails/pull/2818 +# has been merged and released. +RSpec::Matchers.define :execute_queries do |expected = nil| + match do |actual| + counter = ActiveRecord::Assertions::QueryAssertions::SQLCounter.new + + queries = ActiveSupport::Notifications.subscribed(counter, 'sql.active_record') do + actual.call + counter.log + end + + if expected.nil? + queries.count >= 1 + else + queries.count == expected + end + end + + supports_block_expectations +end diff --git a/spec/system/settings/applications_spec.rb b/spec/system/settings/applications_spec.rb index 62656c2b8e0a41..024a6403555d77 100644 --- a/spec/system/settings/applications_spec.rb +++ b/spec/system/settings/applications_spec.rb @@ -104,7 +104,12 @@ def submit_form let(:redis_pipeline_stub) { instance_double(Redis::PipelinedConnection, publish: nil) } let!(:access_token) { Fabricate(:accessible_access_token, application: application) } - before { stub_redis_pipeline } + before do + # Disable wrapstodon to avoid redis calls that we don't want to stub + Setting.wrapstodon = false + + stub_redis_pipeline + end it 'destroys the record and tells the broader universe about that' do visit settings_applications_path diff --git a/spec/system/wrapstodon_spec.rb b/spec/system/wrapstodon_spec.rb new file mode 100644 index 00000000000000..67d544dd47eb46 --- /dev/null +++ b/spec/system/wrapstodon_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Wrapstodon' do + describe 'Viewing a wrapstodon' do + let(:generated_annual_report) { AnnualReport.new(user.account, Time.current.year).generate } + let(:user) { Fabricate :user } + + context 'when signed in' do + before { sign_in user } + + it 'visits the wrap page and renders the web app' do + visit public_wrapstodon_path(account_username: user.account.username, year: generated_annual_report.year, share_key: generated_annual_report.share_key) + + expect(page) + .to have_css('#wrapstodon') + .and have_private_cache_control + end + end + end +end diff --git a/streaming/Dockerfile b/streaming/Dockerfile index 3a12007f68b19b..74c0c42aae1ccd 100644 --- a/streaming/Dockerfile +++ b/streaming/Dockerfile @@ -86,7 +86,6 @@ WORKDIR /opt/mastodon # Copy Node package configuration files from build system to container COPY package.json yarn.lock .yarnrc.yml /opt/mastodon/ -COPY .yarn /opt/mastodon/.yarn # Copy Streaming source code from build system to container COPY ./streaming /opt/mastodon/streaming diff --git a/stylelint.config.js b/stylelint.config.js index b1f34b7f19061b..9ccee477484b60 100644 --- a/stylelint.config.js +++ b/stylelint.config.js @@ -31,7 +31,7 @@ module.exports = { }, overrides: [ { - 'files': ['app/javascript/styles/entrypoints/mailer.scss'], + files: ['app/javascript/styles/entrypoints/mailer.scss'], rules: { 'property-no-unknown': [ true, @@ -42,5 +42,14 @@ module.exports = { ], }, }, + { + files: ['app/javascript/**/*.module.scss'], + rules: { + 'selector-pseudo-class-no-unknown': [ + true, + { ignorePseudoClasses: ['global'] }, + ] + } + }, ], }; diff --git a/vite.config.mts b/vite.config.mts index 3250e8f7861fc2..0a10b30a5b32ef 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -120,6 +120,8 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => { manifest: true, outDir, assetsDir: 'assets', + assetsInlineLimit: (filePath, _) => + /\.woff2?$/.exec(filePath) ? false : undefined, rollupOptions: { input: await findEntrypoints(), output: { @@ -154,6 +156,14 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => { }, }, }, + experimental: { + /** + * Setting this causes Vite to not rely on the base config for import URLs, + * and instead uses import.meta.url, which is what we want for proper CDN support. + * @see https://github.com/mastodon/mastodon/pull/37310 + */ + renderBuiltUrl: () => undefined, + }, worker: { format: 'es', }, diff --git a/yarn.lock b/yarn.lock index e02d60a683bf89..b8e7c57192124e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2825,7 +2825,7 @@ __metadata: msw: "npm:^2.12.1" msw-storybook-addon: "npm:^2.0.6" path-complete-extname: "npm:^1.0.0" - playwright: "npm:^1.56.1" + playwright: "npm:^1.57.0" postcss-preset-env: "npm:^10.1.5" prettier: "npm:^3.3.3" prop-types: "npm:^15.8.1" @@ -2860,7 +2860,7 @@ __metadata: stylelint-config-prettier-scss: "npm:^1.0.0" stylelint-config-standard-scss: "npm:^16.0.0" substring-trie: "npm:^1.0.2" - tesseract.js: "npm:^6.0.0" + tesseract.js: "npm:^7.0.0" tiny-queue: "npm:^0.2.1" twitter-text: "npm:3.1.0" typescript: "npm:~5.9.0" @@ -2871,7 +2871,7 @@ __metadata: vite-plugin-manifest-sri: "npm:^0.2.0" vite-plugin-pwa: "npm:^1.0.2" vite-plugin-svgr: "npm:^4.3.0" - vite-tsconfig-paths: "npm:^5.1.4" + vite-tsconfig-paths: "npm:^6.0.0" vitest: "npm:^4.0.5" wicg-inert: "npm:^3.1.2" workbox-expiration: "npm:^7.3.0" @@ -4854,7 +4854,7 @@ __metadata: languageName: node linkType: hard -"@vitest/browser@npm:4.0.13, @vitest/browser@npm:^4.0.5": +"@vitest/browser@npm:4.0.13": version: 4.0.13 resolution: "@vitest/browser@npm:4.0.13" dependencies: @@ -4872,6 +4872,24 @@ __metadata: languageName: node linkType: hard +"@vitest/browser@npm:^4.0.5": + version: 4.0.15 + resolution: "@vitest/browser@npm:4.0.15" + dependencies: + "@vitest/mocker": "npm:4.0.15" + "@vitest/utils": "npm:4.0.15" + magic-string: "npm:^0.30.21" + pixelmatch: "npm:7.1.0" + pngjs: "npm:^7.0.0" + sirv: "npm:^3.0.2" + tinyrainbow: "npm:^3.0.3" + ws: "npm:^8.18.3" + peerDependencies: + vitest: 4.0.15 + checksum: 10c0/b74c1ab5b03a494b1a91e270417a794e616d3d9d5002de816b6a9913073fdf5939ca63b30a37e4e865cb9402b8682254facaf4b854d002b65b6ea85fccf38253 + languageName: node + linkType: hard + "@vitest/coverage-v8@npm:^4.0.5": version: 4.0.13 resolution: "@vitest/coverage-v8@npm:4.0.13" @@ -4962,6 +4980,25 @@ __metadata: languageName: node linkType: hard +"@vitest/mocker@npm:4.0.15": + version: 4.0.15 + resolution: "@vitest/mocker@npm:4.0.15" + dependencies: + "@vitest/spy": "npm:4.0.15" + estree-walker: "npm:^3.0.3" + magic-string: "npm:^0.30.21" + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + checksum: 10c0/7a164aa25daab3e92013ec95aab5c5778e805b1513e266ce6c00e0647eb9f7b281e33fcaf0d9d2aed88321079183b60c1aeab90961f618c24e2e3a5143308850 + languageName: node + linkType: hard + "@vitest/pretty-format@npm:3.2.4": version: 3.2.4 resolution: "@vitest/pretty-format@npm:3.2.4" @@ -4980,6 +5017,15 @@ __metadata: languageName: node linkType: hard +"@vitest/pretty-format@npm:4.0.15": + version: 4.0.15 + resolution: "@vitest/pretty-format@npm:4.0.15" + dependencies: + tinyrainbow: "npm:^3.0.3" + checksum: 10c0/d863f3818627b198f9c66515f8faa200e76a1c30c7f2b25ac805e253204ae51abbfa6b6211c58b2d75e1a273a2e6925e3a8fa480ebfa9c479d75a19815e1cbea + languageName: node + linkType: hard + "@vitest/runner@npm:4.0.13": version: 4.0.13 resolution: "@vitest/runner@npm:4.0.13" @@ -5017,6 +5063,13 @@ __metadata: languageName: node linkType: hard +"@vitest/spy@npm:4.0.15": + version: 4.0.15 + resolution: "@vitest/spy@npm:4.0.15" + checksum: 10c0/22319cead44964882d9e7bd197a9cd317c945ff75a4a9baefe06c32c5719d4cb887e86b4560d79716765f288a93a6c76e78e3eeab0000f24b2236dab678b7c34 + languageName: node + linkType: hard + "@vitest/ui@npm:^4.0.5": version: 4.0.13 resolution: "@vitest/ui@npm:4.0.13" @@ -5055,6 +5108,16 @@ __metadata: languageName: node linkType: hard +"@vitest/utils@npm:4.0.15": + version: 4.0.15 + resolution: "@vitest/utils@npm:4.0.15" + dependencies: + "@vitest/pretty-format": "npm:4.0.15" + tinyrainbow: "npm:^3.0.3" + checksum: 10c0/2ef661c2c2359ae956087f0b728b6a0f7555cd10524a7def27909f320f6b8ba00560ee1bd856bf68d4debc01020cea21b200203a5d2af36c44e94528c5587aee + languageName: node + linkType: hard + "abbrev@npm:^3.0.0": version: 3.0.1 resolution: "abbrev@npm:3.0.1" @@ -5553,6 +5616,15 @@ __metadata: languageName: node linkType: hard +"baseline-browser-mapping@npm:^2.9.0": + version: 2.9.2 + resolution: "baseline-browser-mapping@npm:2.9.2" + bin: + baseline-browser-mapping: dist/cli.js + checksum: 10c0/4f9be09e20261ed26f19e9b95454dcb8d8371b87983c57cd9f70b9572e9b3053577f0d8d6d91297bdb605337747680686e22f62522a6e57ae2488fcacf641188 + languageName: node + linkType: hard + "bidi-js@npm:^1.0.3": version: 1.0.3 resolution: "bidi-js@npm:1.0.3" @@ -5641,7 +5713,22 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.24.0, browserslist@npm:^4.24.4, browserslist@npm:^4.25.1, browserslist@npm:^4.26.0": +"browserslist@npm:^4.24.0, browserslist@npm:^4.25.1": + version: 4.28.1 + resolution: "browserslist@npm:4.28.1" + dependencies: + baseline-browser-mapping: "npm:^2.9.0" + caniuse-lite: "npm:^1.0.30001759" + electron-to-chromium: "npm:^1.5.263" + node-releases: "npm:^2.0.27" + update-browserslist-db: "npm:^1.2.0" + bin: + browserslist: cli.js + checksum: 10c0/545a5fa9d7234e3777a7177ec1e9134bb2ba60a69e6b95683f6982b1473aad347c77c1264ccf2ac5dea609a9731fbfbda6b85782bdca70f80f86e28a402504bd + languageName: node + linkType: hard + +"browserslist@npm:^4.24.4, browserslist@npm:^4.26.0": version: 4.26.2 resolution: "browserslist@npm:4.26.2" dependencies: @@ -5766,6 +5853,13 @@ __metadata: languageName: node linkType: hard +"caniuse-lite@npm:^1.0.30001759": + version: 1.0.30001759 + resolution: "caniuse-lite@npm:1.0.30001759" + checksum: 10c0/b0f415960ba34995cda18e0d25c4e602f6917b9179290a76bdd0311423505b78cc93e558a90c98a22a1cc6b1781ab720ef6beea24ec7e29a1c1164ca72eac3a2 + languageName: node + linkType: hard + "chai@npm:^5.2.0": version: 5.2.0 resolution: "chai@npm:5.2.0" @@ -6564,6 +6658,13 @@ __metadata: languageName: node linkType: hard +"electron-to-chromium@npm:^1.5.263": + version: 1.5.266 + resolution: "electron-to-chromium@npm:1.5.266" + checksum: 10c0/74ada92ada1ace76ec5b7da8a9cc2d7f03db122a64ac8e12ae30eba3e358ffec443c0c5265bc6edcdeebfa73f449b21c361080c064eb1eec437db2d71fc03248 + languageName: node + linkType: hard + "emoji-mart@npm:emoji-mart-lazyload@latest": version: 3.0.1-j resolution: "emoji-mart-lazyload@npm:3.0.1-j" @@ -9372,7 +9473,14 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0, lru-cache@npm:^11.2.2": +"lru-cache@npm:^11.0.0, lru-cache@npm:^11.2.2": + version: 11.2.4 + resolution: "lru-cache@npm:11.2.4" + checksum: 10c0/4a24f9b17537619f9144d7b8e42cd5a225efdfd7076ebe7b5e7dc02b860a818455201e67fbf000765233fe7e339d3c8229fc815e9b58ee6ede511e07608c19b2 + languageName: node + linkType: hard + +"lru-cache@npm:^11.1.0": version: 11.2.2 resolution: "lru-cache@npm:11.2.2" checksum: 10c0/72d7831bbebc85e2bdefe01047ee5584db69d641c48d7a509e86f66f6ee111b30af7ec3bd68a967d47b69a4b1fa8bbf3872630bd06a63b6735e6f0a5f1c8e83d @@ -9914,6 +10022,13 @@ __metadata: languageName: node linkType: hard +"node-releases@npm:^2.0.27": + version: 2.0.27 + resolution: "node-releases@npm:2.0.27" + checksum: 10c0/f1e6583b7833ea81880627748d28a3a7ff5703d5409328c216ae57befbced10ce2c991bea86434e8ec39003bd017f70481e2e5f8c1f7e0a7663241f81d6e00e2 + languageName: node + linkType: hard + "nopt@npm:^8.0.0": version: 8.1.0 resolution: "nopt@npm:8.1.0" @@ -10513,27 +10628,27 @@ __metadata: languageName: node linkType: hard -"playwright-core@npm:1.56.1": - version: 1.56.1 - resolution: "playwright-core@npm:1.56.1" +"playwright-core@npm:1.57.0": + version: 1.57.0 + resolution: "playwright-core@npm:1.57.0" bin: playwright-core: cli.js - checksum: 10c0/ffd40142b99c68678b387445d5b42f1fee4ab0b65d983058c37f342e5629f9cdbdac0506ea80a0dfd41a8f9f13345bad54e9a8c35826ef66dc765f4eb3db8da7 + checksum: 10c0/798e35d83bf48419a8c73de20bb94d68be5dde68de23f95d80a0ebe401e3b83e29e3e84aea7894d67fa6c79d2d3d40cc5bcde3e166f657ce50987aaa2421b6a9 languageName: node linkType: hard -"playwright@npm:^1.56.1": - version: 1.56.1 - resolution: "playwright@npm:1.56.1" +"playwright@npm:^1.57.0": + version: 1.57.0 + resolution: "playwright@npm:1.57.0" dependencies: fsevents: "npm:2.3.2" - playwright-core: "npm:1.56.1" + playwright-core: "npm:1.57.0" dependenciesMeta: fsevents: optional: true bin: playwright: cli.js - checksum: 10c0/8e9965aede86df0f4722063385748498977b219630a40a10d1b82b8bd8d4d4e9b6b65ecbfa024331a30800163161aca292fb6dd7446c531a1ad25f4155625ab4 + checksum: 10c0/ab03c99a67b835bdea9059f516ad3b6e42c21025f9adaa161a4ef6bc7ca716dcba476d287140bb240d06126eb23f889a8933b8f5f1f1a56b80659d92d1358899 languageName: node linkType: hard @@ -13244,16 +13359,16 @@ __metadata: languageName: node linkType: hard -"tesseract.js-core@npm:^6.0.0": - version: 6.0.0 - resolution: "tesseract.js-core@npm:6.0.0" - checksum: 10c0/c04be8bbaa296be658664496754f21e857bdffff84113f08adf02f03a1f84596d68b3542ed2fda4a6dc138abb84b09b30ab07c04ee5950879e780876d343955f +"tesseract.js-core@npm:^7.0.0": + version: 7.0.0 + resolution: "tesseract.js-core@npm:7.0.0" + checksum: 10c0/c1afee9f8aecf994bc4714fd879e57d04b995849345532872bdc3d8c82a59c4ebbb0acde14d2b24e6a3aec27cafee3d18931f2744496d603ae36241290108e17 languageName: node linkType: hard -"tesseract.js@npm:^6.0.0": - version: 6.0.1 - resolution: "tesseract.js@npm:6.0.1" +"tesseract.js@npm:^7.0.0": + version: 7.0.0 + resolution: "tesseract.js@npm:7.0.0" dependencies: bmp-js: "npm:^0.1.0" idb-keyval: "npm:^6.2.0" @@ -13261,10 +13376,10 @@ __metadata: node-fetch: "npm:^2.6.9" opencollective-postinstall: "npm:^2.0.3" regenerator-runtime: "npm:^0.13.3" - tesseract.js-core: "npm:^6.0.0" - wasm-feature-detect: "npm:^1.2.11" + tesseract.js-core: "npm:^7.0.0" + wasm-feature-detect: "npm:^1.8.0" zlibjs: "npm:^0.3.1" - checksum: 10c0/1d73bb1fbc00c8629756d9594989d8bbfabda657a8cad84922ad68eb0f073148c82845bf71a882e5d2427a46edb5a470356864e60562c7a8442bddd70251435a + checksum: 10c0/daf5b153a9a06e0ab3365b33f4cc323e375d3a8b86a7df98031c19047623451aa3bfb293c295b0ebecd5c0781e42e43469d93ed4e7ba8a868b7a457120e609a1 languageName: node linkType: hard @@ -13893,6 +14008,20 @@ __metadata: languageName: node linkType: hard +"update-browserslist-db@npm:^1.2.0": + version: 1.2.2 + resolution: "update-browserslist-db@npm:1.2.2" + dependencies: + escalade: "npm:^3.2.0" + picocolors: "npm:^1.1.1" + peerDependencies: + browserslist: ">= 4.21.0" + bin: + update-browserslist-db: cli.js + checksum: 10c0/39c3ea08b397ffc8dc3a1c517f5c6ed5cc4179b5e185383dab9bf745879623c12062a2e6bf4f9427cc59389c7bfa0010e86858b923c1e349e32fdddd9b043bb2 + languageName: node + linkType: hard + "uri-js@npm:^4.2.2": version: 4.4.1 resolution: "uri-js@npm:4.4.1" @@ -13947,11 +14076,11 @@ __metadata: linkType: hard "use-sync-external-store@npm:^1.4.0": - version: 1.4.0 - resolution: "use-sync-external-store@npm:1.4.0" + version: 1.6.0 + resolution: "use-sync-external-store@npm:1.6.0" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - checksum: 10c0/ec011a5055962c0f6b509d6e78c0b143f8cd069890ae370528753053c55e3b360d3648e76cfaa854faa7a59eb08d6c5fb1015e60ffde9046d32f5b2a295acea5 + checksum: 10c0/35e1179f872a53227bdf8a827f7911da4c37c0f4091c29b76b1e32473d1670ebe7bcd880b808b7549ba9a5605c233350f800ffab963ee4a4ee346ee983b6019b languageName: node linkType: hard @@ -14036,9 +14165,9 @@ __metadata: languageName: node linkType: hard -"vite-tsconfig-paths@npm:^5.1.4": - version: 5.1.4 - resolution: "vite-tsconfig-paths@npm:5.1.4" +"vite-tsconfig-paths@npm:^6.0.0": + version: 6.0.1 + resolution: "vite-tsconfig-paths@npm:6.0.1" dependencies: debug: "npm:^4.1.1" globrex: "npm:^0.1.2" @@ -14048,7 +14177,7 @@ __metadata: peerDependenciesMeta: vite: optional: true - checksum: 10c0/6228f23155ea25d92b1e1702284cf8dc52ad3c683c5ca691edd5a4c82d2913e7326d00708cef1cbfde9bb226261df0e0a12e03ef1d43b6a92d8f02b483ef37e3 + checksum: 10c0/c0702f1d2b9d2e3e6ebb44d8e9c27b17b1102e86946ab54b6bbd290419b134e84df4e451b55db973bc97d9de5689df6f67e479633df20244aa0c62ffd0b16e43 languageName: node linkType: hard @@ -14187,7 +14316,7 @@ __metadata: languageName: node linkType: hard -"wasm-feature-detect@npm:^1.2.11": +"wasm-feature-detect@npm:^1.8.0": version: 1.8.0 resolution: "wasm-feature-detect@npm:1.8.0" checksum: 10c0/2cb43e91bbf7aa7c121bc76b3133de3ab6dc4f482acc1d2dc46c528e8adb7a51c72df5c2aacf1d219f113c04efd1706f18274d5790542aa5dd49e0644e3ee665