Compare commits

...

79 Commits

Author SHA1 Message Date
Claire
b2506cc110 Merge pull request #3276 from ClearlyClaire/glitch-soc/merge-4.5
Merge upstream changes up to bb6093c315 into stable-4.5
2025-11-13 17:58:54 +01:00
Claire
d18491b7a7 [Glitch] Fix error when sending new posts
Port 058f704c21 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-13 17:36:50 +01:00
Claire
13330030cd [Glitch] Fix posts coming from public/hashtag streaming being marked as unquotable
Port 55b9d21537 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-13 17:36:21 +01:00
diondiondion
f9012a774c [Glitch] Fix Cmd/Ctrl + Enter not submitting Alt text modal on some browsers
Port 6baa8f2466 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-13 17:31:18 +01:00
Echo
375af385e7 [Glitch] Fix deprecation warning in Vite
Port 28b9e9087a to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-13 17:30:32 +01:00
diondiondion
7e5224a3c0 [Glitch] Fixes blank screen in browsers that don't support Intl.DisplayNames
Port fa2cc409ce to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-13 17:29:41 +01:00
Claire
140d782cba [Glitch] Fix filters not being applied to quotes in detailed view
Port 8a100d84c5 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-13 17:28:58 +01:00
Echo
c7a7ce8ce7 [Glitch] Emoji: Load emoji with hash in URL
Port 9ae0464e8f to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-13 17:28:15 +01:00
diondiondion
afbe0a4860 [Glitch] Fix scroll shift caused by fetch-all-replies alerts
Port 9eea4479e1 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-13 17:27:30 +01:00
diondiondion
febde69d0b [Glitch] Fix dropdown menu not focusing first item when opened via keyboard
Port 30103fd2c8 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-13 17:26:57 +01:00
Claire
85b9a5944d [Glitch] Fix prepared quote not being discarded with contents when replying
Port fbe05d42fb to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-13 17:26:34 +01:00
Claire
585827c14f Merge commit 'bb6093c3153f092736e2f25959e3dcdceeb7bce1' into glitch-soc/merge-4.5 2025-11-13 17:22:33 +01:00
Claire
bb6093c315 Bump version to v4.5.1 2025-11-13 17:12:07 +01:00
Claire
058f704c21 Fix error when sending new posts (#36869) 2025-11-13 17:12:07 +01:00
diondiondion
6baa8f2466 Fix Cmd/Ctrl + Enter not submitting Alt text modal on some browsers (#36866) 2025-11-13 17:12:07 +01:00
github-actions[bot]
e742eff044 New Crowdin Translations for stable-4.5 (automated) (#36864)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-11-13 15:46:34 +01:00
Claire
55b9d21537 Fix posts coming from public/hashtag streaming being marked as unquotable (#36860) 2025-11-13 15:24:58 +01:00
Claire
59f0134578 Fix Update importing old previously-unknown activities and treating them as recent ones (#36848) 2025-11-13 15:24:58 +01:00
Echo
28b9e9087a Fix deprecation warning in Vite (#36849) 2025-11-13 15:24:58 +01:00
diondiondion
fa2cc409ce Fixes blank screen in browsers that don't support Intl.DisplayNames (#36847) 2025-11-13 15:24:58 +01:00
Claire
8a100d84c5 Fix filters not being applied to quotes in detailed view (#36843) 2025-11-13 15:24:58 +01:00
Echo
9ae0464e8f Emoji: Load emoji with hash in URL (#36808) 2025-11-13 15:24:58 +01:00
diondiondion
9eea4479e1 Fix scroll shift caused by fetch-all-replies alerts (#36807) 2025-11-13 15:24:58 +01:00
diondiondion
30103fd2c8 Fix dropdown menu not focusing first item when opened via keyboard (#36804) 2025-11-13 15:24:58 +01:00
Claire
a9a7ad62f1 Update dependency rollup from 4.46.2 to 4.46.4 (#36781) 2025-11-13 15:24:58 +01:00
Claire
ea663cf7c7 Fix /api/v1/statuses/:id/context sometimes returing Mastodon-Async-Refresh without result_count (#36779) 2025-11-13 15:24:58 +01:00
Claire
fbe05d42fb Fix prepared quote not being discarded with contents when replying (#36778) 2025-11-13 15:24:58 +01:00
Claire
29ae9c9c4b Add 4.5.x to the list of supported branches (#36761) 2025-11-06 17:12:41 +01:00
Claire
4684d5e69b Merge pull request #3269 from ClearlyClaire/glitch-soc/merge-4.5
Merge upstream changes up to 26c78392f8
2025-11-06 13:09:16 +01:00
Claire
3ca92c4ae2 Merge commit '26c78392f88afccebb8a88b68bc9994a9ce12648' into glitch-soc/merge-4.5 2025-11-06 12:49:01 +01:00
Claire
81b1a34d96 Merge pull request #3267 from ClearlyClaire/glitch-soc/merge-4.5
Merge upstream changes up to 048430f4e8
2025-11-06 12:45:08 +01:00
Claire
26c78392f8 Bump version to v4.5.0 (#36732) 2025-11-06 12:39:07 +01:00
Echo
caecc88247 [Glitch] Fix: correctly dismisses announcement when viewed
Port d45b4db1d7 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-06 12:20:19 +01:00
Echo
bed4ca26e2 [Glitch] Add default visualizer for audio upload without poster
Port ef3a95affc to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-06 12:19:58 +01:00
diondiondion
5d108e95d7 [Glitch] Fix spoiler toggle button being able to submit compose form
Port 3e6a9371b0 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-06 12:18:21 +01:00
Claire
8a2d38a47b Merge commit '048430f4e8010672eb667f1ccf2372571cfb97dd' into glitch-soc/merge-4.5 2025-11-06 12:16:55 +01:00
github-actions[bot]
048430f4e8 New Crowdin Translations for stable-4.5 (automated) (#36745)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-11-06 11:37:54 +01:00
Echo
d45b4db1d7 Fix: correctly dismisses announcement when viewed (#36750) 2025-11-06 11:23:46 +01:00
Echo
ef3a95affc Add default visualizer for audio upload without poster (#36734) 2025-11-06 10:34:12 +01:00
diondiondion
3e6a9371b0 Fix spoiler toggle button being able to submit compose form (#36736) 2025-11-06 10:34:12 +01:00
Claire
7ab0cfd637 Merge pull request #3266 from ClearlyClaire/glitch-soc/merge-4.5
Port missing changes to stable-4.5
2025-11-05 11:27:29 +01:00
Claire
b33bcc8be6 Merge pull request #3265 from ClearlyClaire/glitch-soc/merge-4.5
Merge upstream changes up to e91c764590
2025-11-05 10:52:40 +01:00
Claire
cdad6ee0c9 [Glitch] Change paste-link-to-quote loading state from generic loading bar to compose placeholder
Port cfdd9396c0 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-05 10:09:28 +01:00
Claire
2d958cb909 [Glitch] Change quote action to error instead of insert link in Private Mentions
Port ba498ae779 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-05 10:09:12 +01:00
Echo
949f15e200 [Glitch] Quote Posts: Add notifications for DMs and private posts
Port 5bae08d1ff to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-05 10:09:02 +01:00
Claire
105a2d64a7 [Glitch] Fix Skeleton placeholders being animated when setting to reduce animations is enabled
Port 0b50789c5b to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-05 10:08:49 +01:00
Claire
1c17990413 [Glitch] Fix quote dropdown menu item in detailed status view
Port a978e37f4c to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-05 10:08:37 +01:00
Claire
0a8f96d3be [Glitch] Remove option to disable access to local topic feeds for logged-in users
Port dd708298a8 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-05 10:08:24 +01:00
Claire
1ec8e42dbb [Glitch] Disable paste-link-to-quote flow when composing Private Mentions
Port 6d53ca63d6 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-11-05 10:08:08 +01:00
Claire
7ea3c6a039 Merge commit 'e91c7645905972d9663a0d944b133ff24670bce2' into glitch-soc/merge-4.5 2025-11-05 10:02:33 +01:00
Claire
e91c764590 Bump version to v4.5.0-rc.3 2025-11-05 09:59:00 +01:00
Claire
cfdd9396c0 Change paste-link-to-quote loading state from generic loading bar to compose placeholder (#36695) 2025-11-05 09:59:00 +01:00
Claire
ba498ae779 Change quote action to error instead of insert link in Private Mentions (#36721) 2025-11-05 09:59:00 +01:00
Echo
5bae08d1ff Quote Posts: Add notifications for DMs and private posts (#36696) 2025-11-05 09:59:00 +01:00
Echo
5253527ec4 Add CSS Module support (#36637) 2025-11-05 09:59:00 +01:00
Claire
0b50789c5b Fix Skeleton placeholders being animated when setting to reduce animations is enabled (#36716) 2025-11-05 09:59:00 +01:00
Claire
a978e37f4c Fix quote dropdown menu item in detailed status view (#36704) 2025-11-05 09:59:00 +01:00
Claire
dd708298a8 Remove option to disable access to local topic feeds for logged-in users (#36703) 2025-11-05 09:59:00 +01:00
renovate[bot]
449eb03f11 chore(deps): update dependency sidekiq to v8.0.9 (#36699)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-05 09:59:00 +01:00
renovate[bot]
1baede0a7c chore(deps): update dependency brakeman to v7.1.1 (#35434)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-05 09:59:00 +01:00
renovate[bot]
a7ecfc1ca5 fix(deps): update dependency @rails/ujs to v7.1.600 (#36634)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-05 09:59:00 +01:00
Claire
e62baacfc1 Increase number of quote approval job retries (#36698) 2025-11-05 09:59:00 +01:00
Rachael Wright-Munn
b5a6feb3bf Move "Privacy and reach" from "Public profile" to top-level navigation (#27294) 2025-11-05 09:59:00 +01:00
Claire
05964f571b Prevent creation of Private Mentions quoting someone who is not mentioned (#36689) 2025-11-05 09:59:00 +01:00
Claire
16a54f7158 Fix issuance of quote approval for remote private statuses (#36693) 2025-11-05 09:59:00 +01:00
Claire
6d53ca63d6 Disable paste-link-to-quote flow when composing Private Mentions (#36690) 2025-11-05 09:59:00 +01:00
renovate[bot]
93acfdd7d3 chore(deps): update dependency irb to v1.15.3 (#36682)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-05 09:59:00 +01:00
renovate[bot]
a209b8e544 chore(deps): update dependency rubyzip to v3.2.2 (#36687)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-05 09:59:00 +01:00
Claire
ca0c5e7a79 Merge pull request #3258 from ClearlyClaire/glitch-soc/merge-4.5
Merge upstream changes up to af4c372ab2 into stable-4.5
2025-10-31 16:35:41 +01:00
diondiondion
10a81a2f43 [Glitch] Fix initially selected language in Rules panel, hide selector when no alternative translations exist
Port aa579ce286 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-31 16:12:38 +01:00
diondiondion
9f8fce3c47 [Glitch] Show error when submitting empty post rather than failing silently
Port 214d59bd37 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-10-31 16:12:03 +01:00
Claire
5f5e6ca031 Merge commit 'af4c372ab22b636742833592235d18418604fcc1' into glitch-soc/merge-4.5 2025-10-31 16:04:05 +01:00
Claire
af4c372ab2 Bump version to v4.5.0-rc.2 2025-10-31 16:01:06 +01:00
diondiondion
aa579ce286 Fix initially selected language in Rules panel, hide selector when no alternative translations exist (#36672) 2025-10-31 16:01:06 +01:00
github-actions[bot]
adfabf8c80 New Crowdin Translations for stable-4.5 (automated) (#36670)
Co-authored-by: GitHub Actions <noreply@github.com>
2025-10-31 14:51:18 +01:00
renovate[bot]
ea710df180 chore(deps): update dependency axios to v1.13.1 (#36633)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-31 14:09:36 +01:00
renovate[bot]
e1b6e28829 chore(deps): update dependency libvips to v8.17.3 (#36654)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-31 14:09:36 +01:00
diondiondion
214d59bd37 Show error when submitting empty post rather than failing silently (#36650) 2025-10-31 14:09:36 +01:00
Claire
e4291e9b05 Fix SMTP configuration with mail 2.9.0 (#36646) 2025-10-31 14:09:36 +01:00
223 changed files with 4028 additions and 1219 deletions

View File

@@ -20,7 +20,7 @@ jobs:
# Only tag with latest when ran against the latest stable branch
# This needs to be updated after each minor version release
flavor: |
latest=${{ startsWith(github.ref, 'refs/tags/v4.3.') }}
latest=${{ startsWith(github.ref, 'refs/tags/v4.5.') }}
tags: |
type=pep440,pattern={{raw}}
type=pep440,pattern=v{{major}}.{{minor}}
@@ -37,7 +37,7 @@ jobs:
# Only tag with latest when ran against the latest stable branch
# This needs to be updated after each minor version release
flavor: |
latest=${{ startsWith(github.ref, 'refs/tags/v4.3.') }}
latest=${{ startsWith(github.ref, 'refs/tags/v4.5.') }}
tags: |
type=pep440,pattern={{raw}}
type=pep440,pattern=v{{major}}.{{minor}}

View File

@@ -2,17 +2,33 @@
All notable changes to this project will be documented in this file.
## [4.5.0] - UNRELEASED
## [4.5.1] - 2025-11-13
### Fixes
- Fix Cmd/Ctrl + Enter not submitting Alt text modal on some browsers (#36866 by @diondiondion)
- Fix posts coming from public/hashtag streaming being marked as unquotable (#36860 and #36869 by @ClearlyClaire)
- Fix old previously-undiscovered posts being treated as new when receiving an `Update` (#36848 by @ClearlyClaire)
- Fix blank screen in browsers that don't support `Intl.DisplayNames` (#36847 by @diondiondion)
- Fix filters not being applied to quotes in detailed view (#36843 by @ClearlyClaire)
- Fix scroll shift caused by fetch-all-replies alerts (#36807 by @diondiondion)
- Fix dropdown menu not focusing first item when opened via keyboard (#36804 by @diondiondion)
- Fix assets build issue on arch64 (#36781 by @ClearlyClaire)
- Fix `/api/v1/statuses/:id/context` sometimes returing `Mastodon-Async-Refresh` without `result_count` (#36779 by @ClearlyClaire)
- Fix prepared quote not being discarded with contents when replying (#36778 by @ClearlyClaire)
## [4.5.0] - 2025-11-06
### Added
- **Add support for allowing and authoring quotes** (#35355, #35578, #35614, #35618, #35624, #35626, #35652, #35629, #35665, #35653, #35670, #35677, #35690, #35697, #35689, #35699, #35700, #35701, #35709, #35714, #35713, #35715, #35725, #35749, #35769, #35780, #35762, #35804, #35808, #35805, #35819, #35824, #35828, #35822, #35835, #35865, #35860, #35832, #35891, #35894, #35895, #35820, #35917, #35924, #35925, #35914, #35930, #35941, #35939, #35948, #35955, #35967, #35990, #35991, #35975, #35971, #36002, #35986, #36031, #36034, #36038, #36054, #36052, #36055, #36065, #36068, #36083, #36087, #36080, #36091, #36090, #36118, #36119, #36128, #36094, #36129, #36138, #36132, #36151, #36158, #36171, #36194, #36220, #36169, #36130, #36249, #36153, #36299, #36291, #36301, #36315, #36317, #36364, #36383, #36381, #36459, #36464, #36461, #36516, #36528, #36549, #36550 and #36559 by @ChaosExAnima, @ClearlyClaire, @Lycolia, @diondiondion, and @tribela)\
- **Add support for allowing and authoring quotes** (#35355, #35578, #35614, #35618, #35624, #35626, #35652, #35629, #35665, #35653, #35670, #35677, #35690, #35697, #35689, #35699, #35700, #35701, #35709, #35714, #35713, #35715, #35725, #35749, #35769, #35780, #35762, #35804, #35808, #35805, #35819, #35824, #35828, #35822, #35835, #35865, #35860, #35832, #35891, #35894, #35895, #35820, #35917, #35924, #35925, #35914, #35930, #35941, #35939, #35948, #35955, #35967, #35990, #35991, #35975, #35971, #36002, #35986, #36031, #36034, #36038, #36054, #36052, #36055, #36065, #36068, #36083, #36087, #36080, #36091, #36090, #36118, #36119, #36128, #36094, #36129, #36138, #36132, #36151, #36158, #36171, #36194, #36220, #36169, #36130, #36249, #36153, #36299, #36291, #36301, #36315, #36317, #36364, #36383, #36381, #36459, #36464, #36461, #36516, #36528, #36549, #36550, #36559, #36693, #36704, #36690, #36689, #36696, #36721, #36695 and #36736 by @ChaosExAnima, @ClearlyClaire, @Lycolia, @diondiondion, and @tribela)\
This includes a revamp of the composer interface.\
See https://blog.joinmastodon.org/2025/09/introducing-quote-posts/ for a user-centric overview of the feature, and https://docs.joinmastodon.org/client/quotes/ for API documentation.
- **Add support for fetching and refreshing replies to the web UI** (#35210, #35496, #35575, #35500, #35577, #35602, #35603, #35654, #36141, #36237, #36172, #36256, #36271, #36334, #36382, #36239, #36484, #36481, #36583, #36627 and #36547 by @ClearlyClaire, @diondiondion, @Gargron and @renchap)
- **Add ability to block words in usernames** (#35407, #35655, and #35806 by @ClearlyClaire and @Gargron)
- Add ability to individually disable local or remote feeds for visitors or logged-in users `disabled` value to server setting for live and topic feeds, as well as user permission to bypass that (#36338, #36467, #36497, #36563, #36577, #36585, and #36607 by @ClearlyClaire)\
This splits the `timeline_preview` setting into four more granular settings controlling live feeds and topic (hashtag, trending link) feeds, with 3 values each: `public`, `authenticated`, `disabled`.\
- Add ability to individually disable local or remote feeds for visitors or logged-in users `disabled` value to server setting for live and topic feeds, as well as user permission to bypass that (#36338, #36467, #36497, #36563, #36577, #36585, #36607 and #36703 by @ClearlyClaire)\
This splits the `timeline_preview` setting into four more granular settings controlling live feeds and topic (hashtag, trending link) feeds.\
The setting for local topic feeds has 2 values: `public` and `authenticated`. Every other setting has 3 values: `public`, `authenticated`, `disabled`.\
When `disabled`, users with the “View live and topic feeds” will still be able to view them.
- Add support for displaying of quote posts in Moderator UI (#35964 by @ThisIsMissEm)
- Add support for displaying link previews for Admin UI (#35958 by @ThisIsMissEm)
@@ -20,21 +36,22 @@ All notable changes to this project will be documented in this file.
- Add support for `Update` activities on converted object types (#36322 by @ClearlyClaire)
- Add support for dynamic viewport height (#36272 by @e1berd)
- Add support for numeric-based URIs for new local accounts (#32724, #36304, #36316, and #36365 by @ClearlyClaire)
- Add default visualizer for audio upload without poster (#36734 by @ChaosExAnima)
- Add Traditional Mongolian to posting languages (#36196 by @shimon1024)
- Add example post with manual quote approval policy to `dev:populate_sample_data` (#36099 by @ClearlyClaire)
- Add server-side support for handling posts with a quote policy allowing followers to quote (#36093 and #36127 by @ClearlyClaire)
- Add schema.org markup to SEO-enabled posts (#36075 by @Gargron)
- Add migration to fill unset default quote policy based on default post privacy (#36041 by @ClearlyClaire)
- Add “Posting defaults” setting page, moving existing settings from “Other” (#35896, #36033, #35966, #35969, and #36084 by @ClearlyClaire and @diondiondion)
- Added emoji from Twemoji v16 (#36501 and #36530 by @ChaosExAnima)
- Add feature to select custom emoji rendering (#35229, #35282, #35253, #35424, #35473, #35483, #35505, #35568, #35605, #35659, #35664, #35739, #35985, #36051, #36071, #36137, #36165, #36248, #36262, #36275, #36293, #36341, #36342, #36366, #36377, #36378, #36385, #36393, #36397, #36403, #36413, #36410, #36454, #36402, #36503, #36502, #36532, #36603, #36409, #36638 and #36750 by @ChaosExAnima, @ClearlyClaire and @braddunbar)\
This also completely reworks the processing and rendering of emojis and server-rendered HTML in statuses and other places.
- Add support for exposing conversation context for new public conversations according to FEP-7888 (#35959 and #36064 by @ClearlyClaire and @jesseplusplus)
- Add digest re-check before removing followers in synchronization mechanism (#34273 by @ClearlyClaire)
- Add “Posting defaults” setting page, moving existing settings from “Other” (#35896, #36033, #35966, #35969, and #36084 by @ClearlyClaire and @diondiondion)
- Add support for displaying Valkey version on admin dashboard (#35785 by @ykzts)
- Add delivery failure tracking and handling to FASP jobs (#35625, #35628, and #35723 by @oneiros)
- Add example of quote post with a preview card to development sample data (#35616 by @ClearlyClaire)
- Add second set of blocked text that applies to accounts regardless of account age for spam-blocking (#35563 by @ClearlyClaire)
- Added emoji from Twemoji v16 (#36501 and #36530 by @ChaosExAnima)
- Add feature to select custom emoji rendering (#35229, #35282, #35253, #35424, #35473, #35483, #35505, #35568, #35605, #35659, #35664, #35739, #35985, #36051, #36071, #36137, #36165, #36248, #36262, #36275, #36293, #36341, #36342, #36366, #36377, #36378, #36385, #36393, #36397, #36403, #36413, #36410, #36454, #36402, #36503, #36502, #36532, #36603, #36409 and #36638 by @ChaosExAnima, @ClearlyClaire and @braddunbar)\
This also completely reworks the processing and rendering of emojis and server-rendered HTML in statuses and other places.
### Changed
@@ -43,6 +60,9 @@ All notable changes to this project will be documented in this file.
- Change appearance settings to introduce new Advanced settings section (#36496 and #36506 by @diondiondion)
- Change display of blocked and muted quoted users (#36619 by @ClearlyClaire)\
This adds `blocked_account`, `blocked_domain` and `muted_account` values to the `state` attribute of `Quote` and `ShallowQuote` REST API entities.
- Change submitting an empty post to show an error rather than failing silently (#36650 by @diondiondion)
- Change "Privacy and reach" settings from "Public profile" to their own top-level category (#27294 by @ChaelCodes)
- Change number of times quote verification is retried to better deal with temporary failures (#36698 by @ClearlyClaire)
- Change display of content warnings in Admin UI (#35935 by @ThisIsMissEm)
- Change styling of column banners (#36531 by @ClearlyClaire)
- Change recommended Node version to 24 (LTS) (#36539 by @renchap)
@@ -70,9 +90,11 @@ All notable changes to this project will be documented in this file.
- Fix relationship not being fetched to evaluate whether to show a quote post (#36517 by @ClearlyClaire)
- Fix rendering of poll options in status history modal (#35633 by @ThisIsMissEm)
- Fix “mute” button being displayed to unauthenticated visitors in hashtag dropdown (#36353 by @mkljczk)
- Fix initially selected language in Rules panel, hide selector when no alternative translations exist (#36672 by @diondiondion)
- Fix URL comparison for mentions in case of empty path (#36613 and #36626 by @ClearlyClaire)
- Fix hashtags not being picked up when full-width hash sign is used (#36103 and #36625 by @ClearlyClaire and @Gargron)
- Fix layout of severed relationships when purged events are listed (#36593 by @mejofi)
- Fix Skeleton placeholders being animated when setting to reduce animations is enabled (#36716 by @ClearlyClaire)
- Fix vacuum tasks being interrupted by a single batch failure (#36606 by @Gargron)
- Fix handling of unreachable network error for search services (#36587 by @mjankowski)
- Fix bookmarks export when a bookmarked status is soft-deleted (#36576 by @ClearlyClaire)

View File

@@ -183,7 +183,7 @@ FROM build AS libvips
# libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"]
# renovate: datasource=github-releases depName=libvips packageName=libvips/libvips
ARG VIPS_VERSION=8.17.2
ARG VIPS_VERSION=8.17.3
# libvips download URL, change with [--build-arg VIPS_URL="https://github.com/libvips/libvips/releases/download"]
ARG VIPS_URL=https://github.com/libvips/libvips/releases/download

View File

@@ -128,7 +128,7 @@ GEM
blurhash (0.1.8)
bootsnap (1.18.6)
msgpack (~> 1.2)
brakeman (7.0.2)
brakeman (7.1.1)
racc
browser (6.2.0)
builder (3.3.0)
@@ -224,7 +224,7 @@ GEM
mail (~> 2.7)
email_validator (2.2.4)
activemodel
erb (5.1.1)
erb (5.1.3)
erubi (1.13.1)
et-orbi (1.4.0)
tzinfo
@@ -337,7 +337,7 @@ GEM
activesupport (>= 3.0)
nokogiri (>= 1.6)
io-console (0.8.1)
irb (1.15.2)
irb (1.15.3)
pp (>= 0.6.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
@@ -621,7 +621,7 @@ GEM
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.8.1)
rack (3.2.3)
rack (3.2.4)
rack-attack (6.8.0)
rack (>= 1.0, < 4)
rack-cors (3.0.0)
@@ -691,7 +691,7 @@ GEM
readline (~> 0.0)
rdf-normalize (0.7.0)
rdf (~> 3.3)
rdoc (6.15.0)
rdoc (6.15.1)
erb
psych (>= 4.0.0)
tsort
@@ -791,7 +791,7 @@ GEM
ruby-vips (2.2.5)
ffi (~> 1.12)
logger
rubyzip (3.2.1)
rubyzip (3.2.2)
rufus-scheduler (3.9.2)
fugit (~> 1.1, >= 1.11.1)
safety_net_attestation (0.5.0)
@@ -805,7 +805,7 @@ GEM
securerandom (0.4.1)
shoulda-matchers (6.5.0)
activesupport (>= 5.2.0)
sidekiq (8.0.8)
sidekiq (8.0.9)
connection_pool (>= 2.5.0)
json (>= 2.9.0)
logger (>= 1.6.2)

View File

@@ -15,7 +15,8 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
| Version | Supported |
| ------- | ---------------- |
| 4.5.x | Yes |
| 4.4.x | Yes |
| 4.3.x | Yes |
| 4.3.x | Until 2026-05-06 |
| 4.2.x | Until 2026-01-08 |
| < 4.2 | No |

View File

@@ -9,7 +9,7 @@ class ActivityPub::QuoteAuthorizationsController < ActivityPub::BaseController
before_action :set_quote_authorization
def show
expires_in 30.seconds, public: true if @quote.status.distributable? && public_fetch_mode?
expires_in 30.seconds, public: true if @quote.quoted_status.distributable? && public_fetch_mode?
render json: @quote, serializer: ActivityPub::QuoteAuthorizationSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end
@@ -23,7 +23,7 @@ class ActivityPub::QuoteAuthorizationsController < ActivityPub::BaseController
@quote = Quote.accepted.where(quoted_account: @account).find(params[:id])
return not_found unless @quote.status.present? && @quote.quoted_status.present?
authorize @quote.status, :show?
authorize @quote.quoted_status, :show?
rescue Mastodon::NotPermittedError
not_found
end

View File

@@ -66,7 +66,7 @@ class Api::V1::StatusesController < Api::BaseController
if async_refresh.running?
add_async_refresh_header(async_refresh)
elsif !current_account.nil? && @status.should_fetch_replies?
add_async_refresh_header(AsyncRefresh.create(refresh_key))
add_async_refresh_header(AsyncRefresh.create(refresh_key, count_results: true))
WorkerBatch.new.within do |batch|
batch.connect(refresh_key, threshold: 1.0)

View File

@@ -5,6 +5,7 @@ import { throttle } from 'lodash';
import api from 'flavours/glitch/api';
import { browserHistory } from 'flavours/glitch/components/router';
import { countableText } from 'flavours/glitch/features/compose/util/counter';
import { search as emojiSearch } from 'flavours/glitch/features/emoji/emoji_mart_search_light';
import { tagHistory } from 'flavours/glitch/settings';
import { recoverHashtags } from 'flavours/glitch/utils/hashtag';
@@ -57,7 +58,6 @@ export const COMPOSE_ADVANCED_OPTIONS_CHANGE = 'COMPOSE_ADVANCED_OPTIONS_CHANGE'
export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE';
export const COMPOSE_CONTENT_TYPE_CHANGE = 'COMPOSE_CONTENT_TYPE_CHANGE';
export const COMPOSE_LANGUAGE_CHANGE = 'COMPOSE_LANGUAGE_CHANGE';
@@ -93,6 +93,7 @@ const messages = defineMessages({
open: { id: 'compose.published.open', defaultMessage: 'Open' },
published: { id: 'compose.published.body', defaultMessage: 'Post published.' },
saved: { id: 'compose.saved.body', defaultMessage: 'Post saved.' },
blankPostError: { id: 'compose.error.blank_post', defaultMessage: 'Post can\'t be blank.' },
});
export const ensureComposeIsVisible = (getState) => {
@@ -215,7 +216,15 @@ export function submitCompose(overridePrivacy = null, successCallback = undefine
const spoilers = getState().getIn(['compose', 'spoiler']) || getState().getIn(['local_settings', 'always_show_spoilers_field']);
const spoiler_text = spoilers ? getState().getIn(['compose', 'spoiler_text'], '') : '';
if (!(status?.length || media.size !== 0 || (hasQuote && spoiler_text?.length))) {
const fulltext = `${spoiler_text ?? ''}${countableText(status ?? '')}`;
const hasText = fulltext.trim().length > 0;
if (!(hasText || media.size !== 0 || (hasQuote && spoiler_text?.length))) {
dispatch(showAlert({
message: messages.blankPostError,
}));
dispatch(focusCompose());
return;
}
@@ -815,13 +824,6 @@ export function changeComposeSpoilerText(text) {
};
}
export function changeComposeVisibility(value) {
return {
type: COMPOSE_VISIBILITY_CHANGE,
value,
};
}
export function insertEmojiCompose(position, emoji, needsSpace) {
return {
type: COMPOSE_EMOJI_INSERT,

View File

@@ -13,10 +13,11 @@ import {
} from 'flavours/glitch/store/typed_functions';
import type { ApiQuotePolicy } from '../api_types/quotes';
import type { Status } from '../models/status';
import type { Status, StatusVisibility } from '../models/status';
import type { RootState } from '../store';
import { showAlert } from './alerts';
import { focusCompose } from './compose';
import { changeCompose, focusCompose } from './compose';
import { importFetchedStatuses } from './importer';
import { openModal } from './modal';
@@ -41,6 +42,10 @@ const messages = defineMessages({
id: 'quote_error.unauthorized',
defaultMessage: 'You are not authorized to quote this post.',
},
quoteErrorPrivateMention: {
id: 'quote_error.private_mentions',
defaultMessage: 'Quoting is not allowed with direct mentions.',
},
});
type SimulatedMediaAttachmentJSON = ApiMediaAttachmentJSON & {
@@ -67,6 +72,39 @@ const simulateModifiedApiResponse = (
return data;
};
export const changeComposeVisibility = createAppThunk(
'compose/visibility_change',
(visibility: StatusVisibility, { dispatch, getState }) => {
if (visibility !== 'direct') {
return visibility;
}
const state = getState();
const quotedStatusId = state.compose.get('quoted_status_id') as
| string
| null;
if (!quotedStatusId) {
return visibility;
}
// Remove the quoted status
dispatch(quoteComposeCancel());
const quotedStatus = state.statuses.get(quotedStatusId) as Status | null;
if (!quotedStatus) {
return visibility;
}
// Append the quoted status URL to the compose text
const url = quotedStatus.get('url') as string;
const text = state.compose.get('text') as string;
if (!text.includes(url)) {
const newText = text.trim() ? `${text}\n\n${url}` : url;
dispatch(changeCompose(newText));
}
return visibility;
},
);
export const changeUploadCompose = createDataLoadingThunk(
'compose/changeUpload',
async (
@@ -130,6 +168,8 @@ export const quoteComposeByStatus = createAppThunk(
if (composeState.get('id')) {
dispatch(showAlert({ message: messages.quoteErrorEdit }));
} else if (composeState.get('privacy') === 'direct') {
dispatch(showAlert({ message: messages.quoteErrorPrivateMention }));
} else if (composeState.get('poll')) {
dispatch(showAlert({ message: messages.quoteErrorPoll }));
} else if (
@@ -173,6 +213,17 @@ export const quoteComposeById = createAppThunk(
},
);
const composeStateForbidsLink = (composeState: RootState['compose']) => {
return (
composeState.get('quoted_status_id') ||
composeState.get('is_submitting') ||
composeState.get('poll') ||
composeState.get('is_uploading') ||
composeState.get('id') ||
composeState.get('privacy') === 'direct'
);
};
export const pasteLinkCompose = createDataLoadingThunk(
'compose/pasteLink',
async ({ url }: { url: string }) => {
@@ -183,15 +234,12 @@ export const pasteLinkCompose = createDataLoadingThunk(
limit: 2,
});
},
(data, { dispatch, getState }) => {
(data, { dispatch, getState, requestId }) => {
const composeState = getState().compose;
if (
composeState.get('quoted_status_id') ||
composeState.get('is_submitting') ||
composeState.get('poll') ||
composeState.get('is_uploading') ||
composeState.get('id')
composeStateForbidsLink(composeState) ||
composeState.get('fetching_link') !== requestId // Request has been cancelled
)
return;
@@ -207,6 +255,17 @@ export const pasteLinkCompose = createDataLoadingThunk(
dispatch(quoteComposeById(data.statuses[0].id));
}
},
{
useLoadingBar: false,
condition: (_, { getState }) =>
!getState().compose.get('fetching_link') &&
!composeStateForbidsLink(getState().compose),
},
);
// Ideally this would cancel the action and the HTTP request, but this is good enough
export const cancelPasteLinkCompose = createAction(
'compose/cancelPasteLinkCompose',
);
export const quoteComposeCancel = createAction('compose/quoteComposeCancel');

View File

@@ -46,11 +46,11 @@ export function importFetchedAccounts(accounts) {
return importAccounts({ accounts: normalAccounts });
}
export function importFetchedStatus(status) {
return importFetchedStatuses([status]);
export function importFetchedStatus(status, options = {}) {
return importFetchedStatuses([status], options);
}
export function importFetchedStatuses(statuses) {
export function importFetchedStatuses(statuses, options = {}) {
return (dispatch, getState) => {
const accounts = [];
const normalStatuses = [];
@@ -58,7 +58,7 @@ export function importFetchedStatuses(statuses) {
const filters = [];
function processStatus(status) {
pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id]), getState().get('local_settings')));
pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id]), { ...options, settings: getState().get('local_settings') }));
pushUnique(accounts, status.account);
if (status.filtered) {

View File

@@ -27,9 +27,12 @@ function stripQuoteFallback(text) {
return wrapper.innerHTML;
}
export function normalizeStatus(status, normalOldStatus, settings) {
export function normalizeStatus(status, normalOldStatus, { settings, bogusQuotePolicy = false }) {
const normalStatus = { ...status };
if (bogusQuotePolicy)
normalStatus.quote_approval = null;
normalStatus.account = status.account.id;
if (status.reblog && status.reblog.id) {
@@ -101,6 +104,8 @@ export function normalizeStatus(status, normalOldStatus, settings) {
}
if (normalOldStatus) {
normalStatus.quote_approval ||= normalOldStatus.quote_approval;
const list = normalOldStatus.get('media_attachments');
if (normalStatus.media_attachments && list) {
normalStatus.media_attachments.forEach(item => {

View File

@@ -204,8 +204,8 @@ export function deleteStatusFail(id, error) {
};
}
export const updateStatus = status => dispatch =>
dispatch(importFetchedStatus(status));
export const updateStatus = (status, { bogusQuotePolicy }) => dispatch =>
dispatch(importFetchedStatus(status, { bogusQuotePolicy }));
export function muteStatus(id) {
return (dispatch) => {

View File

@@ -52,6 +52,9 @@ const randomUpTo = max =>
export const connectTimelineStream = (timelineId, channelName, params = {}, options = {}) => {
const { messages } = getLocale();
// Public streams are currently not returning personalized quote policies
const bogusQuotePolicy = channelName.startsWith('public') || channelName.startsWith('hashtag');
return connectStream(channelName, params, (dispatch, getState) => {
// @ts-ignore
const locale = getState().getIn(['meta', 'locale']);
@@ -97,11 +100,11 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
switch (data.event) {
case 'update':
// @ts-expect-error
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), options.accept));
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), { accept: options.accept, bogusQuotePolicy }));
break;
case 'status.update':
// @ts-expect-error
dispatch(updateStatus(JSON.parse(data.payload)));
dispatch(updateStatus(JSON.parse(data.payload), { bogusQuotePolicy }));
break;
case 'delete':
dispatch(deleteFromTimelines(data.payload));

View File

@@ -33,7 +33,7 @@ export const loadPending = timeline => ({
timeline,
});
export function updateTimeline(timeline, status, accept) {
export function updateTimeline(timeline, status, { accept = undefined, bogusQuotePolicy = false } = {}) {
return (dispatch, getState) => {
if (typeof accept === 'function' && !accept(status)) {
return;
@@ -55,7 +55,7 @@ export function updateTimeline(timeline, status, accept) {
filtered = filters.length > 0;
}
dispatch(importFetchedStatus(status));
dispatch(importFetchedStatus(status, { bogusQuotePolicy }));
dispatch({
type: TIMELINE_UPDATE,

View File

@@ -26,6 +26,7 @@ import {
closeDropdownMenu,
} from 'flavours/glitch/actions/dropdown_menu';
import { openModal, closeModal } from 'flavours/glitch/actions/modal';
import { fetchStatus } from 'flavours/glitch/actions/statuses';
import { CircularProgress } from 'flavours/glitch/components/circular_progress';
import { isUserTouching } from 'flavours/glitch/is_mobile';
import {
@@ -42,16 +43,10 @@ import { IconButton } from './icon_button';
let id = 0;
export interface RenderItemFnHandlers {
onClick: React.MouseEventHandler;
onKeyUp: React.KeyboardEventHandler;
}
export type RenderItemFn<Item = MenuItem> = (
item: Item,
index: number,
handlers: RenderItemFnHandlers,
focusRefCallback?: (c: HTMLAnchorElement | HTMLButtonElement | null) => void,
onClick: React.MouseEventHandler,
) => React.ReactNode;
type ItemClickFn<Item = MenuItem> = (item: Item, index: number) => void;
@@ -101,7 +96,6 @@ export const DropdownMenu = <Item = MenuItem,>({
onItemClick,
}: DropdownMenuProps<Item>) => {
const nodeRef = useRef<HTMLDivElement>(null);
const focusedItemRef = useRef<HTMLElement | null>(null);
useEffect(() => {
const handleDocumentClick = (e: MouseEvent) => {
@@ -163,8 +157,11 @@ export const DropdownMenu = <Item = MenuItem,>({
document.addEventListener('click', handleDocumentClick, { capture: true });
document.addEventListener('keydown', handleKeyDown, { capture: true });
if (focusedItemRef.current && openedViaKeyboard) {
focusedItemRef.current.focus({ preventScroll: true });
if (openedViaKeyboard) {
const firstMenuItem = nodeRef.current?.querySelector<
HTMLAnchorElement | HTMLButtonElement
>('li:first-child > :is(a, button)');
firstMenuItem?.focus({ preventScroll: true });
}
return () => {
@@ -175,13 +172,6 @@ export const DropdownMenu = <Item = MenuItem,>({
};
}, [onClose, openedViaKeyboard]);
const handleFocusedItemRef = useCallback(
(c: HTMLAnchorElement | HTMLButtonElement | null) => {
focusedItemRef.current = c as HTMLElement;
},
[],
);
const handleItemClick = useCallback(
(e: React.MouseEvent | React.KeyboardEvent) => {
const i = Number(e.currentTarget.getAttribute('data-index'));
@@ -207,15 +197,6 @@ export const DropdownMenu = <Item = MenuItem,>({
[onClose, onItemClick, items],
);
const handleItemKeyUp = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
handleItemClick(e);
}
},
[handleItemClick],
);
const nativeRenderItem = (option: Item, i: number) => {
if (!isMenuItem(option)) {
return null;
@@ -232,9 +213,7 @@ export const DropdownMenu = <Item = MenuItem,>({
if (isActionItem(option)) {
element = (
<button
ref={i === 0 ? handleFocusedItemRef : undefined}
onClick={handleItemClick}
onKeyUp={handleItemKeyUp}
data-index={i}
aria-disabled={disabled}
>
@@ -248,9 +227,7 @@ export const DropdownMenu = <Item = MenuItem,>({
target={option.target ?? '_target'}
data-method={option.method}
rel='noopener'
ref={i === 0 ? handleFocusedItemRef : undefined}
onClick={handleItemClick}
onKeyUp={handleItemKeyUp}
data-index={i}
>
<DropdownMenuItemContent item={option} />
@@ -258,13 +235,7 @@ export const DropdownMenu = <Item = MenuItem,>({
);
} else {
element = (
<Link
to={option.to}
ref={i === 0 ? handleFocusedItemRef : undefined}
onClick={handleItemClick}
onKeyUp={handleItemKeyUp}
data-index={i}
>
<Link to={option.to} onClick={handleItemClick} data-index={i}>
<DropdownMenuItemContent item={option} />
</Link>
);
@@ -307,15 +278,7 @@ export const DropdownMenu = <Item = MenuItem,>({
})}
>
{items.map((option, i) =>
renderItemMethod(
option,
i,
{
onClick: handleItemClick,
onKeyUp: handleItemKeyUp,
},
i === 0 ? handleFocusedItemRef : undefined,
),
renderItemMethod(option, i, handleItemClick),
)}
</ul>
)}
@@ -340,6 +303,7 @@ interface DropdownProps<Item extends object | null = MenuItem> {
*/
scrollKey?: string;
status?: ImmutableMap<string, unknown>;
needsStatusRefresh?: boolean;
forceDropdown?: boolean;
renderItem?: RenderItemFn<Item>;
renderHeader?: RenderHeaderFn<Item>;
@@ -363,6 +327,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
placement = 'bottom',
offset = [5, 5],
status,
needsStatusRefresh,
forceDropdown = false,
renderItem,
renderHeader,
@@ -382,6 +347,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
const prefetchAccountId = status
? status.getIn(['account', 'id'])
: undefined;
const statusId = status?.get('id') as string | undefined;
const handleClose = useCallback(() => {
if (buttonRef.current) {
@@ -399,7 +365,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
}, [dispatch, currentId]);
const handleItemClick = useCallback(
(e: React.MouseEvent | React.KeyboardEvent) => {
(e: React.MouseEvent) => {
const i = Number(e.currentTarget.getAttribute('data-index'));
const item = items?.[i];
@@ -420,10 +386,20 @@ export const Dropdown = <Item extends object | null = MenuItem>({
[handleClose, onItemClick, items],
);
const toggleDropdown = useCallback(
(e: React.MouseEvent | React.KeyboardEvent) => {
const { type } = e;
const isKeypressRef = useRef(false);
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
if (e.key === ' ' || e.key === 'Enter') {
isKeypressRef.current = true;
}
}, []);
const unsetIsKeypress = useCallback(() => {
isKeypressRef.current = false;
}, []);
const toggleDropdown = useCallback(
(e: React.MouseEvent) => {
if (open) {
handleClose();
} else {
@@ -436,6 +412,15 @@ export const Dropdown = <Item extends object | null = MenuItem>({
dispatch(fetchRelationships([prefetchAccountId]));
}
if (needsStatusRefresh && statusId) {
dispatch(
fetchStatus(statusId, {
forceFetch: true,
alsoFetchContext: false,
}),
);
}
if (isUserTouching() && !forceDropdown) {
dispatch(
openModal({
@@ -450,10 +435,11 @@ export const Dropdown = <Item extends object | null = MenuItem>({
dispatch(
openDropdownMenu({
id: currentId,
keyboard: type !== 'click',
keyboard: isKeypressRef.current,
scrollKey,
}),
);
isKeypressRef.current = false;
}
}
},
@@ -468,6 +454,8 @@ export const Dropdown = <Item extends object | null = MenuItem>({
items,
forceDropdown,
handleClose,
statusId,
needsStatusRefresh,
],
);
@@ -484,6 +472,9 @@ export const Dropdown = <Item extends object | null = MenuItem>({
const buttonProps = {
disabled,
onClick: toggleDropdown,
onKeyDown: handleKeyDown,
onKeyUp: unsetIsKeypress,
onBlur: unsetIsKeypress,
'aria-expanded': open,
'aria-controls': menuId,
ref: buttonRef,

View File

@@ -58,17 +58,7 @@ export const EditedTimestamp: React.FC<{
}, []);
const renderItem = useCallback(
(
item: HistoryItem,
index: number,
{
onClick,
onKeyUp,
}: {
onClick: React.MouseEventHandler;
onKeyUp: React.KeyboardEventHandler;
},
) => {
(item: HistoryItem, index: number, onClick: React.MouseEventHandler) => {
const formattedDate = (
<RelativeTimestamp
timestamp={item.get('created_at') as string}
@@ -98,7 +88,7 @@ export const EditedTimestamp: React.FC<{
className='dropdown-menu__item edited-timestamp__history__item'
key={item.get('created_at') as string}
>
<button data-index={index} onClick={onClick} onKeyUp={onKeyUp}>
<button data-index={index} onClick={onClick} type='button'>
{label}
</button>
</li>

View File

@@ -8,13 +8,14 @@ import classNames from 'classnames';
import { quoteComposeById } from '@/flavours/glitch/actions/compose_typed';
import { toggleReblog } from '@/flavours/glitch/actions/interactions';
import { openModal } from '@/flavours/glitch/actions/modal';
import { fetchStatus } from '@/flavours/glitch/actions/statuses';
import { quickBoosting } from '@/flavours/glitch/initial_state';
import type { ActionMenuItem } from '@/flavours/glitch/models/dropdown_menu';
import type { Status } from '@/flavours/glitch/models/status';
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
import type { SomeRequired } from '@/flavours/glitch/utils/types';
import type { RenderItemFn, RenderItemFnHandlers } from '../dropdown_menu';
import type { RenderItemFn } from '../dropdown_menu';
import { Dropdown, DropdownMenuItemContent } from '../dropdown_menu';
import { IconButton } from '../icon_button';
@@ -74,18 +75,12 @@ const StandaloneBoostButton: FC<ReblogButtonProps> = ({ status, counters }) => {
);
};
const renderMenuItem: RenderItemFn<ActionMenuItem> = (
item,
index,
handlers,
focusRefCallback,
) => (
const renderMenuItem: RenderItemFn<ActionMenuItem> = (item, index, onClick) => (
<ReblogMenuItem
index={index}
item={item}
handlers={handlers}
onClick={onClick}
key={`${item.text}-${index}`}
focusRefCallback={focusRefCallback}
/>
);
@@ -117,6 +112,7 @@ const BoostOrQuoteMenu: FC<ReblogButtonProps> = ({ status, counters }) => {
const statusId = status.get('id') as string;
const wasBoosted = !!status.get('reblogged');
const quoteApproval = status.get('quote_approval');
const showLoginPrompt = useCallback(() => {
dispatch(
@@ -173,9 +169,16 @@ const BoostOrQuoteMenu: FC<ReblogButtonProps> = ({ status, counters }) => {
dispatch(toggleReblog(status.get('id'), true));
return false;
}
if (quoteApproval === null) {
dispatch(
fetchStatus(statusId, { forceFetch: true, alsoFetchContext: false }),
);
}
return true;
},
[dispatch, isLoggedIn, showLoginPrompt, status],
[dispatch, isLoggedIn, showLoginPrompt, status, quoteApproval, statusId],
);
return (
@@ -208,16 +211,10 @@ const BoostOrQuoteMenu: FC<ReblogButtonProps> = ({ status, counters }) => {
interface ReblogMenuItemProps {
item: ActionMenuItem;
index: number;
handlers: RenderItemFnHandlers;
focusRefCallback?: (c: HTMLAnchorElement | HTMLButtonElement | null) => void;
onClick: React.MouseEventHandler;
}
const ReblogMenuItem: FC<ReblogMenuItemProps> = ({
index,
item,
handlers,
focusRefCallback,
}) => {
const ReblogMenuItem: FC<ReblogMenuItemProps> = ({ index, item, onClick }) => {
const { text, highlighted, disabled } = item;
return (
@@ -227,12 +224,7 @@ const ReblogMenuItem: FC<ReblogMenuItemProps> = ({
})}
key={`${text}-${index}`}
>
<button
{...handlers}
ref={focusRefCallback}
aria-disabled={disabled}
data-index={index}
>
<button onClick={onClick} aria-disabled={disabled} data-index={index}>
<DropdownMenuItemContent item={item} />
</button>
</li>

View File

@@ -374,6 +374,7 @@ class StatusActionBar extends ImmutablePureComponent {
<Dropdown
scrollKey={scrollKey}
status={status}
needsStatusRefresh={quickBoosting && status.get('quote_approval') === null}
items={menu}
icon='ellipsis-h'
size={18}

View File

@@ -49,6 +49,7 @@ export const StatusBanner: React.FC<{
<button
ref={buttonRef}
type='button'
className='link-button'
onClick={onClick}
aria-describedby={descriptionId}

View File

@@ -32,16 +32,38 @@ interface Rule extends BaseRule {
translations?: Record<string, BaseRule>;
}
function getDefaultSelectedLocale(
currentUiLocale: string,
localeOptions: SelectItem[],
) {
const preciseMatch = localeOptions.find(
(option) => option.value === currentUiLocale,
);
if (preciseMatch) {
return preciseMatch.value;
}
const partialLocale = currentUiLocale.split('-')[0];
const partialMatch = localeOptions.find(
(option) => option.value.split('-')[0] === partialLocale,
);
return partialMatch?.value ?? 'default';
}
export const RulesSection: FC<RulesSectionProps> = ({ isLoading = false }) => {
const intl = useIntl();
const [locale, setLocale] = useState(intl.locale);
const rules = useAppSelector((state) => rulesSelector(state, locale));
const localeOptions = useAppSelector((state) =>
localeOptionsSelector(state, intl),
);
const [selectedLocale, setSelectedLocale] = useState(() =>
getDefaultSelectedLocale(intl.locale, localeOptions),
);
const rules = useAppSelector((state) => rulesSelector(state, selectedLocale));
const handleLocaleChange: ChangeEventHandler<HTMLSelectElement> = useCallback(
(e) => {
setLocale(e.currentTarget.value);
setSelectedLocale(e.currentTarget.value);
},
[],
);
@@ -74,25 +96,27 @@ export const RulesSection: FC<RulesSectionProps> = ({ isLoading = false }) => {
))}
</ol>
<div className='rules-languages'>
<label htmlFor='language-select'>
<FormattedMessage
id='about.language_label'
defaultMessage='Language'
/>
</label>
<select onChange={handleLocaleChange} id='language-select'>
{localeOptions.map((option) => (
<option
key={option.value}
value={option.value}
selected={option.value === locale}
>
{option.text}
</option>
))}
</select>
</div>
{localeOptions.length > 1 && (
<div className='rules-languages'>
<label htmlFor='language-select'>
<FormattedMessage
id='about.language_label'
defaultMessage='Language'
/>
</label>
<select onChange={handleLocaleChange} id='language-select'>
{localeOptions.map((option) => (
<option
key={option.value}
value={option.value}
selected={option.value === selectedLocale}
>
{option.text}
</option>
))}
</select>
</div>
)}
</Section>
);
};
@@ -145,9 +169,13 @@ const localeOptionsSelector = createSelector(
},
};
// Use the default locale as a target to translate language names.
const intlLocale = new Intl.DisplayNames(intl.locale, {
type: 'language',
});
const intlLocale =
// Intl.DisplayNames can be undefined in old browsers
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
Intl.DisplayNames &&
(new Intl.DisplayNames(intl.locale, {
type: 'language',
}) as Intl.DisplayNames | undefined);
for (const { translations } of rules) {
for (const locale in translations) {
if (langs[locale]) {
@@ -155,7 +183,7 @@ const localeOptionsSelector = createSelector(
}
langs[locale] = {
value: locale,
text: intlLocale.of(locale) ?? locale,
text: intlLocale?.of(locale) ?? locale,
};
}
}

View File

@@ -330,7 +330,7 @@ export const AltTextModal = forwardRef<ModalRef, Props & Partial<RestoreProps>>(
});
}, [dispatch, setIsSaving, mediaId, onClose, position, description]);
const handleKeyUp = useCallback(
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
@@ -457,7 +457,7 @@ export const AltTextModal = forwardRef<ModalRef, Props & Partial<RestoreProps>>(
id='description'
value={isDetecting ? ' ' : description}
onChange={handleDescriptionChange}
onKeyUp={handleKeyUp}
onKeyDown={handleKeyDown}
lang={lang}
placeholder={intl.formatMessage(
type === 'audio'

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef, useCallback, useState, useId } from 'react';
import { useEffect, useRef, useCallback, useState } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
@@ -22,6 +22,8 @@ import { useAudioVisualizer } from 'flavours/glitch/hooks/useAudioVisualizer';
import { displayMedia, useBlurhash } from 'flavours/glitch/initial_state';
import { playerSettings } from 'flavours/glitch/settings';
import { AudioVisualizer } from './visualizer';
const messages = defineMessages({
play: { id: 'video.play', defaultMessage: 'Play' },
pause: { id: 'video.pause', defaultMessage: 'Pause' },
@@ -116,7 +118,6 @@ export const Audio: React.FC<{
const seekRef = useRef<HTMLDivElement>(null);
const volumeRef = useRef<HTMLDivElement>(null);
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>();
const accessibilityId = useId();
const { audioContextRef, sourceRef, gainNodeRef, playAudio, pauseAudio } =
useAudioContext({ audioElementRef: audioRef });
@@ -538,19 +539,6 @@ export const Audio: React.FC<{
[togglePlay, toggleMute],
);
const springForBand0 = useSpring({
to: { r: 50 + (frequencyBands[0] ?? 0) * 10 },
config: config.wobbly,
});
const springForBand1 = useSpring({
to: { r: 50 + (frequencyBands[1] ?? 0) * 10 },
config: config.wobbly,
});
const springForBand2 = useSpring({
to: { r: 50 + (frequencyBands[2] ?? 0) * 10 },
config: config.wobbly,
});
const progress = Math.min((currentTime / loadedDuration) * 100, 100);
const effectivelyMuted = muted || volume === 0;
@@ -641,81 +629,7 @@ export const Audio: React.FC<{
</div>
<div className='audio-player__controls__play'>
<svg
className='audio-player__visualizer'
viewBox='0 0 124 124'
xmlns='http://www.w3.org/2000/svg'
>
<animated.circle
opacity={0.5}
cx={57}
cy={62.5}
r={springForBand0.r}
fill='var(--player-accent-color)'
/>
<animated.circle
opacity={0.5}
cx={65}
cy={57.5}
r={springForBand1.r}
fill='var(--player-accent-color)'
/>
<animated.circle
opacity={0.5}
cx={63}
cy={66.5}
r={springForBand2.r}
fill='var(--player-accent-color)'
/>
<g clipPath={`url(#${accessibilityId}-clip)`}>
<rect
x={14}
y={14}
width={96}
height={96}
fill={`url(#${accessibilityId}-pattern)`}
/>
<rect
x={14}
y={14}
width={96}
height={96}
fill='var(--player-background-color'
opacity={0.45}
/>
</g>
<defs>
<pattern
id={`${accessibilityId}-pattern`}
patternContentUnits='objectBoundingBox'
width='1'
height='1'
>
<use href={`#${accessibilityId}-image`} />
</pattern>
<clipPath id={`${accessibilityId}-clip`}>
<rect
x={14}
y={14}
width={96}
height={96}
rx={48}
fill='white'
/>
</clipPath>
<image
id={`${accessibilityId}-image`}
href={poster}
width={1}
height={1}
preserveAspectRatio='none'
/>
</defs>
</svg>
<AudioVisualizer frequencyBands={frequencyBands} poster={poster} />
<button
type='button'

View File

@@ -0,0 +1,100 @@
import { useId } from 'react';
import type { FC } from 'react';
import { animated, config, useSpring } from '@react-spring/web';
interface AudioVisualizerProps {
frequencyBands?: number[];
poster?: string;
}
export const AudioVisualizer: FC<AudioVisualizerProps> = ({
frequencyBands = [],
poster,
}) => {
const accessibilityId = useId();
const springForBand0 = useSpring({
to: { r: 50 + (frequencyBands[0] ?? 0) * 10 },
config: config.wobbly,
});
const springForBand1 = useSpring({
to: { r: 50 + (frequencyBands[1] ?? 0) * 10 },
config: config.wobbly,
});
const springForBand2 = useSpring({
to: { r: 50 + (frequencyBands[2] ?? 0) * 10 },
config: config.wobbly,
});
return (
<svg
className='audio-player__visualizer'
viewBox='0 0 124 124'
xmlns='http://www.w3.org/2000/svg'
>
<animated.circle
opacity={0.5}
cx={57}
cy={62.5}
r={springForBand0.r}
fill='var(--player-accent-color)'
/>
<animated.circle
opacity={0.5}
cx={65}
cy={57.5}
r={springForBand1.r}
fill='var(--player-accent-color)'
/>
<animated.circle
opacity={0.5}
cx={63}
cy={66.5}
r={springForBand2.r}
fill='var(--player-accent-color)'
/>
<g clipPath={`url(#${accessibilityId}-clip)`}>
<rect
x={14}
y={14}
width={96}
height={96}
fill={`url(#${accessibilityId}-pattern)`}
/>
<rect
x={14}
y={14}
width={96}
height={96}
fill='var(--player-background-color'
opacity={0.45}
/>
</g>
<defs>
<pattern
id={`${accessibilityId}-pattern`}
patternContentUnits='objectBoundingBox'
width='1'
height='1'
>
<use href={`#${accessibilityId}-image`} />
</pattern>
<clipPath id={`${accessibilityId}-clip`}>
<rect x={14} y={14} width={96} height={96} rx={48} fill='white' />
</clipPath>
<image
id={`${accessibilityId}-image`}
href={poster}
width={1}
height={1}
preserveAspectRatio='none'
/>
</defs>
</svg>
);
};

View File

@@ -138,11 +138,10 @@ class ComposeForm extends ImmutablePureComponent {
};
canSubmit = () => {
const { isSubmitting, isChangingUpload, isUploading, anyMedia, maxChars } = this.props;
const { isSubmitting, isChangingUpload, isUploading, maxChars } = this.props;
const fulltext = this.getFulltextForCharacterCounting();
const isOnlyWhitespace = fulltext.length !== 0 && fulltext.trim().length === 0;
return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > maxChars || (isOnlyWhitespace && !anyMedia));
return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > maxChars);
};
handleSubmit = (e, overridePrivacy = null) => {
@@ -156,7 +155,11 @@ class ComposeForm extends ImmutablePureComponent {
return;
}
this.props.onSubmit(missingAltTextModal && this.props.missingAltText && this.props.privacy !== 'direct', overridePrivacy);
this.props.onSubmit({
missingAltTextModal: missingAltTextModal && this.props.missingAltText && this.props.privacy !== 'direct',
quoteToPrivate: this.props.quoteToPrivate,
overridePrivacy,
});
if (e) {
e.preventDefault();

View File

@@ -0,0 +1,48 @@
import { useCallback } from 'react';
import type { FC } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { cancelPasteLinkCompose } from '@/flavours/glitch/actions/compose_typed';
import { useAppDispatch } from '@/flavours/glitch/store';
import CancelFillIcon from '@/material-icons/400-24px/cancel-fill.svg?react';
import { DisplayName } from 'flavours/glitch/components/display_name';
import { IconButton } from 'flavours/glitch/components/icon_button';
import { Skeleton } from 'flavours/glitch/components/skeleton';
const messages = defineMessages({
quote_cancel: { id: 'status.quote.cancel', defaultMessage: 'Cancel quote' },
});
export const QuotePlaceholder: FC = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const handleQuoteCancel = useCallback(() => {
dispatch(cancelPasteLinkCompose());
}, [dispatch]);
return (
<div className='status__quote'>
<div className='status'>
<div className='status__info'>
<div className='status__avatar'>
<Skeleton width='32px' height='32px' />
</div>
<div className='status__display-name'>
<DisplayName />
</div>
<IconButton
onClick={handleQuoteCancel}
className='status__quote-cancel'
title={intl.formatMessage(messages.quote_cancel)}
icon='cancel-fill'
iconComponent={CancelFillIcon}
/>
</div>
<div className='status__content'>
<Skeleton />
</div>
</div>
</div>
);
};

View File

@@ -7,11 +7,17 @@ import { quoteComposeCancel } from '@/flavours/glitch/actions/compose_typed';
import { QuotedStatus } from '@/flavours/glitch/components/status_quoted';
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
import { QuotePlaceholder } from './quote_placeholder';
export const ComposeQuotedStatus: FC = () => {
const quotedStatusId = useAppSelector(
(state) => state.compose.get('quoted_status_id') as string | null,
);
const isFetchingLink = useAppSelector(
(state) => !!state.compose.get('fetching_link'),
);
const isEditing = useAppSelector((state) => !!state.compose.get('id'));
const quote = useMemo(
@@ -30,7 +36,9 @@ export const ComposeQuotedStatus: FC = () => {
dispatch(quoteComposeCancel());
}, [dispatch]);
if (!quote) {
if (isFetchingLink && !quote) {
return <QuotePlaceholder />;
} else if (!quote) {
return null;
}

View File

@@ -10,6 +10,7 @@ import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import CloseIcon from '@/material-icons/400-20px/close.svg?react';
import SoundIcon from '@/material-icons/400-24px/audio.svg?react';
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
import WarningIcon from '@/material-icons/400-24px/warning.svg?react';
import { undoUploadCompose } from 'flavours/glitch/actions/compose';
@@ -17,7 +18,18 @@ import { openModal } from 'flavours/glitch/actions/modal';
import { Blurhash } from 'flavours/glitch/components/blurhash';
import { Icon } from 'flavours/glitch/components/icon';
import type { MediaAttachment } from 'flavours/glitch/models/media_attachment';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
import {
createAppSelector,
useAppDispatch,
useAppSelector,
} from 'flavours/glitch/store';
import { AudioVisualizer } from '../../audio/visualizer';
const selectUserAvatar = createAppSelector(
[(state) => state.accounts, (state) => state.meta.get('me') as string],
(accounts, myId) => accounts.get(myId)?.avatar_static,
);
export const Upload: React.FC<{
id: string;
@@ -38,6 +50,7 @@ export const Upload: React.FC<{
const sensitive = useAppSelector(
(state) => state.compose.get('sensitive') as boolean,
);
const userAvatar = useAppSelector(selectUserAvatar);
const handleUndoClick = useCallback(() => {
dispatch(undoUploadCompose(id));
@@ -67,6 +80,8 @@ export const Upload: React.FC<{
transform: CSS.Transform.toString(transform),
transition,
};
const preview_url = media.get('preview_url') as string | null;
const blurhash = media.get('blurhash') as string | null;
return (
<div
@@ -85,17 +100,19 @@ export const Upload: React.FC<{
<div
className='compose-form__upload__thumbnail'
style={{
backgroundImage: !sensitive
? `url(${media.get('preview_url') as string})`
: undefined,
backgroundImage:
!sensitive && preview_url ? `url(${preview_url})` : undefined,
backgroundPosition: `${x}% ${y}%`,
}}
>
{sensitive && (
<Blurhash
hash={media.get('blurhash') as string}
className='compose-form__upload__preview'
/>
{sensitive && blurhash && (
<Blurhash hash={blurhash} className='compose-form__upload__preview' />
)}
{!sensitive && !preview_url && (
<div className='compose-form__upload__visualizer'>
<AudioVisualizer poster={userAvatar} />
<Icon id='sound' icon={SoundIcon} />
</div>
)}
<div className='compose-form__upload__actions'>

View File

@@ -5,8 +5,10 @@ import { defineMessages, useIntl } from 'react-intl';
import classNames from 'classnames';
import { changeComposeVisibility } from '@/flavours/glitch/actions/compose';
import { setComposeQuotePolicy } from '@/flavours/glitch/actions/compose_typed';
import {
changeComposeVisibility,
setComposeQuotePolicy,
} from '@/flavours/glitch/actions/compose_typed';
import { openModal } from '@/flavours/glitch/actions/modal';
import type { ApiQuotePolicy } from '@/flavours/glitch/api_types/quotes';
import type { StatusVisibility } from '@/flavours/glitch/api_types/statuses';

View File

@@ -12,6 +12,7 @@ import {
} from 'flavours/glitch/actions/compose';
import { pasteLinkCompose } from 'flavours/glitch/actions/compose_typed';
import { openModal } from 'flavours/glitch/actions/modal';
import { PRIVATE_QUOTE_MODAL_ID } from 'flavours/glitch/features/ui/components/confirmation_modals/private_quote_notify';
import { privacyPreference } from 'flavours/glitch/utils/privacy_preference';
import ComposeForm from '../components/compose_form';
@@ -52,6 +53,10 @@ const mapStateToProps = state => ({
isUploading: state.getIn(['compose', 'is_uploading']),
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
missingAltText: state.getIn(['compose', 'media_attachments']).some(media => ['image', 'gifv'].includes(media.get('type')) && (media.get('description') ?? '').length === 0),
quoteToPrivate:
!!state.getIn(['compose', 'quoted_status_id'])
&& state.getIn(['compose', 'privacy']) === 'private'
&& !state.getIn(['settings', 'dismissed_banners', PRIVATE_QUOTE_MODAL_ID]),
isInReply: state.getIn(['compose', 'in_reply_to']) !== null,
lang: state.getIn(['compose', 'language']),
sideArm: sideArmPrivacy(state),
@@ -65,12 +70,17 @@ const mapDispatchToProps = (dispatch, props) => ({
dispatch(changeCompose(text));
},
onSubmit (missingAltText, overridePrivacy = null) {
onSubmit ({ missingAltText, quoteToPrivate, overridePrivacy = null }) {
if (missingAltText) {
dispatch(openModal({
modalType: 'CONFIRM_MISSING_ALT_TEXT',
modalProps: { overridePrivacy },
}));
} else if (quoteToPrivate) {
dispatch(openModal({
modalType: 'CONFIRM_PRIVATE_QUOTE_NOTIFY',
modalProps: {},
}));
} else {
dispatch(submitCompose(overridePrivacy, (status) => {
if (props.redirectOnSuccess) {

View File

@@ -1,8 +1,7 @@
import { connect } from 'react-redux';
import { changeComposeVisibility } from '../../../actions/compose';
import { openModal, closeModal } from '../../../actions/modal';
import { isUserTouching } from '../../../is_mobile';
import { changeComposeVisibility } from '@/flavours/glitch/actions/compose_typed';
import PrivacyDropdown from '../components/privacy_dropdown';
const mapStateToProps = state => ({

View File

@@ -1,5 +1,5 @@
import { flattenEmojiData } from 'emojibase';
import type { CompactEmoji, FlatCompactEmoji } from 'emojibase';
import type { CompactEmoji, FlatCompactEmoji, Locale } from 'emojibase';
import {
putEmojiData,
@@ -43,9 +43,8 @@ async function fetchAndCheckEtag<ResultType extends object[]>(
if (locale === 'custom') {
url.pathname = '/api/v1/custom_emojis';
} else {
// This doesn't use isDevelopment() as that module loads initial state
// which breaks workers, as they cannot access the DOM.
url.pathname = `/packs${import.meta.env.DEV ? '-dev' : ''}/emoji/${locale}.json`;
const modulePath = await localeToPath(locale);
url.pathname = modulePath;
}
const oldEtag = await loadLatestEtag(locale);
@@ -80,3 +79,19 @@ async function fetchAndCheckEtag<ResultType extends object[]>(
return data;
}
const modules = import.meta.glob<string>(
'../../../../../../node_modules/emojibase-data/**/compact.json',
{
query: '?url',
import: 'default',
},
);
function localeToPath(locale: Locale) {
const key = `../../../../../../node_modules/emojibase-data/${locale}/compact.json`;
if (!modules[key] || typeof modules[key] !== 'function') {
throw new Error(`Unsupported locale: ${locale}`);
}
return modules[key]();
}

View File

@@ -1,11 +1,13 @@
import { useEffect, useState } from 'react';
import type { FC } from 'react';
import { useEffect, useState } from 'react';
import { FormattedDate, FormattedMessage } from 'react-intl';
import { dismissAnnouncement } from '@/flavours/glitch/actions/announcements';
import type { ApiAnnouncementJSON } from '@/flavours/glitch/api_types/announcements';
import { AnimateEmojiProvider } from '@/flavours/glitch/components/emoji/context';
import { EmojiHTML } from '@/flavours/glitch/components/emoji/html';
import { useAppDispatch } from '@/flavours/glitch/store';
import { ReactionsBar } from './reactions';
@@ -22,13 +24,23 @@ export const Announcement: FC<AnnouncementProps> = ({
announcement,
selected,
}) => {
const [unread, setUnread] = useState(!announcement.read);
const { read, id } = announcement;
// Dismiss announcement when it becomes active.
const dispatch = useAppDispatch();
useEffect(() => {
// Only update `unread` marker once the announcement is out of view
if (!selected && unread !== !announcement.read) {
setUnread(!announcement.read);
if (selected && !read) {
dispatch(dismissAnnouncement(id));
}
}, [announcement.read, selected, unread]);
}, [selected, id, dispatch, read]);
// But visually show the announcement as read only when it goes out of view.
const [unread, setUnread] = useState(!read);
useEffect(() => {
if (!selected && unread !== !read) {
setUnread(!read);
}
}, [selected, unread, read]);
return (
<AnimateEmojiProvider className='announcements__item'>

View File

@@ -445,6 +445,7 @@ export const DetailedStatus: React.FC<{
<QuotedStatus
quote={status.get('quote')}
parentQuotePostId={status.get('id')}
contextType='thread'
/>
)}
</>

View File

@@ -295,7 +295,7 @@ export const RefreshController: React.FC<{
if (loadingState === 'loading') {
return (
<div
className='load-more load-gap'
className='load-more load-more--large'
aria-busy
aria-live='polite'
aria-label={intl.formatMessage(messages.loadingInitial)}

View File

@@ -161,7 +161,7 @@ class Status extends ImmutablePureComponent {
componentDidMount () {
attachFullscreenListener(this.onFullScreenChange);
this.props.dispatch(fetchStatus(this.props.params.statusId));
this.props.dispatch(fetchStatus(this.props.params.statusId, { forceFetch: true }));
this._scrollStatusIntoView();
}
@@ -170,7 +170,7 @@ class Status extends ImmutablePureComponent {
let updated = false;
if (props.params.statusId && state.statusId !== props.params.statusId) {
props.dispatch(fetchStatus(props.params.statusId));
props.dispatch(fetchStatus(props.params.statusId, { forceFetch: true }));
update.threadExpanded = undefined;
update.statusId = props.params.statusId;
updated = true;
@@ -329,6 +329,12 @@ class Status extends ImmutablePureComponent {
dispatch(openModal({ modalType: 'COMPOSE_PRIVACY', modalProps: { statusId, onChange: handleChange } }));
};
handleQuote = (status) => {
const { dispatch } = this.props;
dispatch(quoteComposeById(status.get('id')));
};
handleEditClick = (status) => {
const { dispatch, askReplyConfirmation } = this.props;
@@ -659,6 +665,7 @@ class Status extends ImmutablePureComponent {
onDelete={this.handleDeleteClick}
onRevokeQuote={this.handleRevokeQuoteClick}
onQuotePolicyChange={this.handleQuotePolicyChange}
onQuote={this.handleQuote}
onEdit={this.handleEditClick}
onDirect={this.handleDirectClick}
onMention={this.handleMentionClick}

View File

@@ -26,6 +26,7 @@ export const ConfirmationModal: React.FC<
onSecondary?: () => void;
onConfirm: () => void;
closeWhenConfirm?: boolean;
extraContent?: React.ReactNode;
} & BaseConfirmationModalProps
> = ({
title,
@@ -37,6 +38,7 @@ export const ConfirmationModal: React.FC<
secondary,
onSecondary,
closeWhenConfirm = true,
extraContent,
}) => {
const handleClick = useCallback(() => {
if (closeWhenConfirm) {
@@ -57,6 +59,8 @@ export const ConfirmationModal: React.FC<
<div className='safety-action-modal__confirmation'>
<h1>{title}</h1>
{message && <p>{message}</p>}
{extraContent}
</div>
</div>

View File

@@ -0,0 +1,88 @@
import { forwardRef, useCallback, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { submitCompose } from '@/flavours/glitch/actions/compose';
import { changeSetting } from '@/flavours/glitch/actions/settings';
import { CheckBox } from '@/flavours/glitch/components/check_box';
import { useAppDispatch } from '@/flavours/glitch/store';
import { ConfirmationModal } from './confirmation_modal';
import type { BaseConfirmationModalProps } from './confirmation_modal';
import classes from './styles.module.css';
export const PRIVATE_QUOTE_MODAL_ID = 'quote/private_notify';
const messages = defineMessages({
title: {
id: 'confirmations.private_quote_notify.title',
defaultMessage: 'Share with followers and mentioned users?',
},
message: {
id: 'confirmations.private_quote_notify.message',
defaultMessage:
'The person you are quoting and other mentions ' +
"will be notified and will be able to view your post, even if they're not following you.",
},
confirm: {
id: 'confirmations.private_quote_notify.confirm',
defaultMessage: 'Publish post',
},
cancel: {
id: 'confirmations.private_quote_notify.cancel',
defaultMessage: 'Back to editing',
},
});
export const PrivateQuoteNotify = forwardRef<
HTMLDivElement,
BaseConfirmationModalProps
>(
(
{ onClose },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_ref,
) => {
const intl = useIntl();
const [dismiss, setDismissed] = useState(false);
const handleDismissToggle = useCallback(() => {
setDismissed((prev) => !prev);
}, []);
const dispatch = useAppDispatch();
const handleConfirm = useCallback(() => {
dispatch(submitCompose());
if (dismiss) {
dispatch(
changeSetting(['dismissed_banners', PRIVATE_QUOTE_MODAL_ID], true),
);
}
}, [dismiss, dispatch]);
return (
<ConfirmationModal
title={intl.formatMessage(messages.title)}
message={intl.formatMessage(messages.message)}
confirm={intl.formatMessage(messages.confirm)}
cancel={intl.formatMessage(messages.cancel)}
onConfirm={handleConfirm}
onClose={onClose}
extraContent={
<label className={classes.checkbox_wrapper}>
<CheckBox
value='hide'
checked={dismiss}
onChange={handleDismissToggle}
/>{' '}
<FormattedMessage
id='confirmations.private_quote_notify.do_not_show_again'
defaultMessage="Don't show me this message again"
/>
</label>
}
/>
);
},
);
PrivateQuoteNotify.displayName = 'PrivateQuoteNotify';

View File

@@ -0,0 +1,7 @@
.checkbox_wrapper {
display: flex;
align-items: center;
gap: 0.5rem;
margin: 1rem 0;
cursor: pointer;
}

View File

@@ -51,6 +51,7 @@ import MediaModal from './media_modal';
import { ModalPlaceholder } from './modal_placeholder';
import VideoModal from './video_modal';
import { VisibilityModal } from './visibility_modal';
import { PrivateQuoteNotify } from './confirmation_modals/private_quote_notify';
export const MODAL_COMPONENTS = {
'MEDIA': () => Promise.resolve({ default: MediaModal }),
@@ -72,6 +73,7 @@ export const MODAL_COMPONENTS = {
'CONFIRM_LOG_OUT': () => Promise.resolve({ default: ConfirmLogOutModal }),
'CONFIRM_FOLLOW_TO_LIST': () => Promise.resolve({ default: ConfirmFollowToListModal }),
'CONFIRM_MISSING_ALT_TEXT': () => Promise.resolve({ default: ConfirmMissingAltTextModal }),
'CONFIRM_PRIVATE_QUOTE_NOTIFY': () => Promise.resolve({ default: PrivateQuoteNotify }),
'CONFIRM_REVOKE_QUOTE': () => Promise.resolve({ default: ConfirmRevokeQuoteModal }),
'CONFIRM_QUIET_QUOTE': () => Promise.resolve({ default: QuietPostQuoteInfoModal }),
'MUTE': MuteModal,

View File

@@ -128,9 +128,12 @@ export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
const disableVisibility = !!statusId;
const disableQuotePolicy =
visibility === 'private' || visibility === 'direct';
const disablePublicVisibilities: boolean = useAppSelector(
const disablePublicVisibilities = useAppSelector(
selectDisablePublicVisibilities,
);
const isQuotePost = useAppSelector(
(state) => state.compose.get('quoted_status_id') !== null,
);
const visibilityItems = useMemo<SelectItem<StatusVisibility>[]>(() => {
const items: SelectItem<StatusVisibility>[] = [
@@ -315,6 +318,21 @@ export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
id={quoteDescriptionId}
/>
</div>
{isQuotePost && visibility === 'direct' && (
<div className='visibility-modal__quote-warning'>
<FormattedMessage
id='visibility_modal.direct_quote_warning.title'
defaultMessage="Quotes can't be embedded in private mentions"
tagName='h3'
/>
<FormattedMessage
id='visibility_modal.direct_quote_warning.text'
defaultMessage='If you save the current settings, the embedded quote will be converted to a link.'
tagName='p'
/>
</div>
)}
</div>
<div className='dialog-modal__content__actions'>
<Button onClick={onClose} secondary>

View File

@@ -37,7 +37,7 @@ interface InitialStateMeta {
streaming_api_base_url: string;
local_live_feed_access: 'public' | 'authenticated' | 'disabled';
remote_live_feed_access: 'public' | 'authenticated' | 'disabled';
local_topic_feed_access: 'public' | 'authenticated' | 'disabled';
local_topic_feed_access: 'public' | 'authenticated';
remote_topic_feed_access: 'public' | 'authenticated' | 'disabled';
title: string;
show_trends: boolean;
@@ -156,17 +156,21 @@ export const statusPageUrl = getMeta('status_page_url');
export const sso_redirect = getMeta('sso_redirect');
export const termsOfServiceEnabled = getMeta('terms_of_service_enabled');
const displayNames = new Intl.DisplayNames(getMeta('locale'), {
type: 'language',
fallback: 'none',
languageDisplay: 'standard',
});
const displayNames =
// Intl.DisplayNames can be undefined in old browsers
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
Intl.DisplayNames &&
(new Intl.DisplayNames(getMeta('locale'), {
type: 'language',
fallback: 'none',
languageDisplay: 'standard',
}) as Intl.DisplayNames | undefined);
export const languages = initialState?.languages.map((lang) => {
// zh-YUE is not a valid CLDR unicode_language_id
return [
lang[0],
displayNames.of(lang[0].replace('zh-YUE', 'yue')) ?? lang[1],
displayNames?.of(lang[0].replace('zh-YUE', 'yue')) ?? lang[1],
lang[2],
];
});

View File

@@ -1,11 +1,14 @@
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
import {
changeComposeVisibility,
changeUploadCompose,
quoteCompose,
quoteComposeCancel,
setComposeQuotePolicy,
} from 'flavours/glitch/actions/compose_typed';
pasteLinkCompose,
cancelPasteLinkCompose,
} from '@/flavours/glitch/actions/compose_typed';
import { timelineDelete } from 'flavours/glitch/actions/timelines_typed';
import {
@@ -39,7 +42,6 @@ import {
COMPOSE_SENSITIVITY_CHANGE,
COMPOSE_SPOILERNESS_CHANGE,
COMPOSE_SPOILER_TEXT_CHANGE,
COMPOSE_VISIBILITY_CHANGE,
COMPOSE_LANGUAGE_CHANGE,
COMPOSE_COMPOSING_CHANGE,
COMPOSE_CONTENT_TYPE_CHANGE,
@@ -119,6 +121,7 @@ const initialState = ImmutableMap({
quoted_status_id: null,
quote_policy: 'public',
default_quote_policy: 'public', // Set in hydration.
fetching_link: null,
});
const initialPoll = ImmutableMap({
@@ -391,7 +394,11 @@ const calculateProgress = (loaded, total) => Math.min(Math.round((loaded / total
/** @type {import('@reduxjs/toolkit').Reducer<typeof initialState>} */
export const composeReducer = (state = initialState, action) => {
if (changeUploadCompose.fulfilled.match(action)) {
if (changeComposeVisibility.match(action)) {
return state
.set('privacy', action.payload)
.set('idempotencyKey', uuid());
} else if (changeUploadCompose.fulfilled.match(action)) {
return state
.set('is_changing_upload', false)
.update('media_attachments', list => list.map(item => {
@@ -407,15 +414,27 @@ export const composeReducer = (state = initialState, action) => {
return state.set('is_changing_upload', false);
} else if (quoteCompose.match(action)) {
const status = action.payload;
const isDirect = state.get('privacy') === 'direct';
return state
.set('quoted_status_id', status.get('id'))
.set('quoted_status_id', isDirect ? null : status.get('id'))
.set('spoiler', status.get('sensitive'))
.set('spoiler_text', status.get('spoiler_text'))
.update('privacy', (visibility) => ['public', 'unlisted'].includes(visibility) && status.get('visibility') === 'private' ? 'private' : visibility);
.update('privacy', (visibility) => {
if (['public', 'unlisted'].includes(visibility) && status.get('visibility') === 'private') {
return 'private';
}
return visibility;
});
} else if (quoteComposeCancel.match(action)) {
return state.set('quoted_status_id', null);
} else if (setComposeQuotePolicy.match(action)) {
return state.set('quote_policy', action.payload);
} else if (pasteLinkCompose.pending.match(action)) {
return state.set('fetching_link', action.meta.requestId);
} else if (pasteLinkCompose.fulfilled.match(action) || pasteLinkCompose.rejected.match(action)) {
return action.meta.requestId === state.get('fetching_link') ? state.set('fetching_link', null) : state;
} else if (cancelPasteLinkCompose.match(action)) {
return state.set('fetching_link', null);
}
switch(action.type) {
@@ -462,10 +481,6 @@ export const composeReducer = (state = initialState, action) => {
return state
.set('spoiler_text', action.text)
.set('idempotencyKey', uuid());
case COMPOSE_VISIBILITY_CHANGE:
return state
.set('privacy', action.value)
.set('idempotencyKey', uuid());
case COMPOSE_CONTENT_TYPE_CHANGE:
return state
.set('content_type', action.value)
@@ -490,6 +505,7 @@ export const composeReducer = (state = initialState, action) => {
map.set('caretPosition', null);
map.set('preselectDate', new Date());
map.set('idempotencyKey', uuid());
map.set('quoted_status_id', null);
map.update('media_attachments', list => list.filter(media => media.get('unattached')));

View File

@@ -32,7 +32,7 @@ function getStatusResultFunction(
};
}
if (statusBase.get('isLoading')) {
if (statusBase.get('isLoading') && !statusBase.get('content')) {
return {
status: null,
loadingState: 'loading',
@@ -75,7 +75,7 @@ function getStatusResultFunction(
map.set('matched_filters', filtered);
map.set('matched_media_filters', mediaFiltered);
}),
loadingState: 'complete'
loadingState: statusBase.get('isLoading') ? 'loading' : 'complete'
};
}

View File

@@ -42,7 +42,7 @@ interface AppThunkConfig {
}
export type AppThunkApi = Pick<
GetThunkAPI<AppThunkConfig>,
'getState' | 'dispatch'
'getState' | 'dispatch' | 'requestId'
>;
interface AppThunkOptions<Arg> {
@@ -60,7 +60,7 @@ type AppThunk<Arg = void, Returned = void> = (
type AppThunkCreator<Arg = void, Returned = void, ExtraArg = unknown> = (
arg: Arg,
api: AppThunkApi,
api: Pick<AppThunkApi, 'getState' | 'dispatch'>,
extra?: ExtraArg,
) => Returned;
@@ -143,10 +143,10 @@ export function createAsyncThunk<Arg = void, Returned = void>(
name,
async (
arg: Arg,
{ getState, dispatch, fulfillWithValue, rejectWithValue },
{ getState, dispatch, requestId, fulfillWithValue, rejectWithValue },
) => {
try {
const result = await creator(arg, { dispatch, getState });
const result = await creator(arg, { dispatch, getState, requestId });
return fulfillWithValue(result, {
useLoadingBar: options.useLoadingBar,
@@ -280,10 +280,11 @@ export function createDataLoadingThunk<
return createAsyncThunk<Args, Returned>(
name,
async (arg, { getState, dispatch }) => {
async (arg, { getState, dispatch, requestId }) => {
const data = await loadData(arg, {
dispatch,
getState,
requestId,
});
if (!onData) return data as Returned;
@@ -291,6 +292,7 @@ export function createDataLoadingThunk<
const result = await onData(data, {
dispatch,
getState,
requestId,
discardLoadData: discardLoadDataInPayload,
actionArg: arg,
});

View File

@@ -1330,6 +1330,10 @@ a.sparkline {
line-height: 1;
width: 100%;
animation: skeleton 1.2s ease-in-out infinite;
.reduce-motion & {
animation: none;
}
}
@keyframes skeleton {

View File

@@ -775,16 +775,43 @@
padding: 8px;
}
&__preview {
&__preview,
&__visualizer {
position: absolute;
width: 100%;
height: 100%;
border-radius: 6px;
z-index: -1;
top: 0;
}
&__preview {
border-radius: 6px;
inset-inline-start: 0;
}
&__visualizer {
padding: 16px;
box-sizing: border-box;
.audio-player__visualizer {
margin: 0 auto;
display: block;
height: 100%;
}
.icon {
position: absolute;
top: 50%;
inset-inline-start: 50%;
transform: translate(-50%, -50%);
opacity: 0.75;
color: var(--player-foreground-color);
filter: var(--overlay-icon-shadow);
width: 48px;
height: 48px;
}
}
&__thumbnail {
width: 100%;
height: 100%;
@@ -3217,20 +3244,21 @@ a.account__display-name {
}
.column__alert {
--alert-height: 54px;
position: sticky;
bottom: 0;
z-index: 10;
box-sizing: border-box;
display: grid;
grid-template-rows: minmax(var(--alert-height), max-content);
align-items: end;
width: 100%;
max-width: 360px;
padding: 1rem;
margin: auto auto 0;
overflow: clip;
&:empty {
padding: 0;
}
pointer-events: none;
@media (max-width: #{$mobile-menu-breakpoint - 1}) {
// Compensate for mobile menubar
@@ -3241,6 +3269,7 @@ a.account__display-name {
// Make all nested alerts occupy the same space
// rather than stack
grid-area: 1 / 1;
pointer-events: initial;
}
}
@@ -4542,13 +4571,19 @@ a.status-card {
box-sizing: border-box;
text-decoration: none;
&:hover {
background: var(--on-surface-color);
&--large {
padding-block: 32px;
}
&:focus-visible {
outline: 2px solid $ui-button-focus-outline-color;
outline-offset: -2px;
&:is(button) {
&:hover {
background: var(--on-surface-color);
}
&:focus-visible {
outline: 2px solid $ui-button-focus-outline-color;
outline-offset: -2px;
}
}
.icon {
@@ -5980,6 +6015,34 @@ a.status-card {
}
}
.visibility-modal {
&__quote-warning {
color: var(--nested-card-text);
background:
/* This is a bit of a silly hack for layering two background colours
* since --nested-card-background is too transparent for a tooltip */
linear-gradient(
var(--nested-card-background),
var(--nested-card-background)
),
linear-gradient(var(--background-color), var(--background-color));
border: var(--nested-card-border);
padding: 16px;
border-radius: 4px;
h3 {
font-weight: 500;
margin-bottom: 4px;
color: $darker-text-color;
}
p {
font-size: 0.8em;
color: $dark-text-color;
}
}
}
.visibility-dropdown {
&__overlay[data-popper-placement] {
z-index: 9999;

View File

@@ -5,6 +5,7 @@ import { throttle } from 'lodash';
import api from 'mastodon/api';
import { browserHistory } from 'mastodon/components/router';
import { countableText } from 'mastodon/features/compose/util/counter';
import { search as emojiSearch } from 'mastodon/features/emoji/emoji_mart_search_light';
import { tagHistory } from 'mastodon/settings';
@@ -55,7 +56,6 @@ export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT';
export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE';
export const COMPOSE_LANGUAGE_CHANGE = 'COMPOSE_LANGUAGE_CHANGE';
@@ -88,6 +88,7 @@ const messages = defineMessages({
open: { id: 'compose.published.open', defaultMessage: 'Open' },
published: { id: 'compose.published.body', defaultMessage: 'Post published.' },
saved: { id: 'compose.saved.body', defaultMessage: 'Post saved.' },
blankPostError: { id: 'compose.error.blank_post', defaultMessage: 'Post can\'t be blank.' },
});
export const ensureComposeIsVisible = (getState) => {
@@ -197,7 +198,15 @@ export function submitCompose(successCallback) {
const hasQuote = !!getState().getIn(['compose', 'quoted_status_id']);
const spoiler_text = getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : '';
if (!(status?.length || media.size !== 0 || (hasQuote && spoiler_text?.length))) {
const fulltext = `${spoiler_text ?? ''}${countableText(status ?? '')}`;
const hasText = fulltext.trim().length > 0;
if (!(hasText || media.size !== 0 || (hasQuote && spoiler_text?.length))) {
dispatch(showAlert({
message: messages.blankPostError,
}));
dispatch(focusCompose());
return;
}
@@ -786,13 +795,6 @@ export function changeComposeSpoilerText(text) {
};
}
export function changeComposeVisibility(value) {
return {
type: COMPOSE_VISIBILITY_CHANGE,
value,
};
}
export function insertEmojiCompose(position, emoji, needsSpace) {
return {
type: COMPOSE_EMOJI_INSERT,

View File

@@ -13,10 +13,11 @@ import {
} from 'mastodon/store/typed_functions';
import type { ApiQuotePolicy } from '../api_types/quotes';
import type { Status } from '../models/status';
import type { Status, StatusVisibility } from '../models/status';
import type { RootState } from '../store';
import { showAlert } from './alerts';
import { focusCompose } from './compose';
import { changeCompose, focusCompose } from './compose';
import { importFetchedStatuses } from './importer';
import { openModal } from './modal';
@@ -41,6 +42,10 @@ const messages = defineMessages({
id: 'quote_error.unauthorized',
defaultMessage: 'You are not authorized to quote this post.',
},
quoteErrorPrivateMention: {
id: 'quote_error.private_mentions',
defaultMessage: 'Quoting is not allowed with direct mentions.',
},
});
type SimulatedMediaAttachmentJSON = ApiMediaAttachmentJSON & {
@@ -67,6 +72,39 @@ const simulateModifiedApiResponse = (
return data;
};
export const changeComposeVisibility = createAppThunk(
'compose/visibility_change',
(visibility: StatusVisibility, { dispatch, getState }) => {
if (visibility !== 'direct') {
return visibility;
}
const state = getState();
const quotedStatusId = state.compose.get('quoted_status_id') as
| string
| null;
if (!quotedStatusId) {
return visibility;
}
// Remove the quoted status
dispatch(quoteComposeCancel());
const quotedStatus = state.statuses.get(quotedStatusId) as Status | null;
if (!quotedStatus) {
return visibility;
}
// Append the quoted status URL to the compose text
const url = quotedStatus.get('url') as string;
const text = state.compose.get('text') as string;
if (!text.includes(url)) {
const newText = text.trim() ? `${text}\n\n${url}` : url;
dispatch(changeCompose(newText));
}
return visibility;
},
);
export const changeUploadCompose = createDataLoadingThunk(
'compose/changeUpload',
async (
@@ -130,6 +168,8 @@ export const quoteComposeByStatus = createAppThunk(
if (composeState.get('id')) {
dispatch(showAlert({ message: messages.quoteErrorEdit }));
} else if (composeState.get('privacy') === 'direct') {
dispatch(showAlert({ message: messages.quoteErrorPrivateMention }));
} else if (composeState.get('poll')) {
dispatch(showAlert({ message: messages.quoteErrorPoll }));
} else if (
@@ -173,6 +213,17 @@ export const quoteComposeById = createAppThunk(
},
);
const composeStateForbidsLink = (composeState: RootState['compose']) => {
return (
composeState.get('quoted_status_id') ||
composeState.get('is_submitting') ||
composeState.get('poll') ||
composeState.get('is_uploading') ||
composeState.get('id') ||
composeState.get('privacy') === 'direct'
);
};
export const pasteLinkCompose = createDataLoadingThunk(
'compose/pasteLink',
async ({ url }: { url: string }) => {
@@ -183,15 +234,12 @@ export const pasteLinkCompose = createDataLoadingThunk(
limit: 2,
});
},
(data, { dispatch, getState }) => {
(data, { dispatch, getState, requestId }) => {
const composeState = getState().compose;
if (
composeState.get('quoted_status_id') ||
composeState.get('is_submitting') ||
composeState.get('poll') ||
composeState.get('is_uploading') ||
composeState.get('id')
composeStateForbidsLink(composeState) ||
composeState.get('fetching_link') !== requestId // Request has been cancelled
)
return;
@@ -207,6 +255,17 @@ export const pasteLinkCompose = createDataLoadingThunk(
dispatch(quoteComposeById(data.statuses[0].id));
}
},
{
useLoadingBar: false,
condition: (_, { getState }) =>
!getState().compose.get('fetching_link') &&
!composeStateForbidsLink(getState().compose),
},
);
// Ideally this would cancel the action and the HTTP request, but this is good enough
export const cancelPasteLinkCompose = createAction(
'compose/cancelPasteLinkCompose',
);
export const quoteComposeCancel = createAction('compose/quoteComposeCancel');

View File

@@ -46,11 +46,11 @@ export function importFetchedAccounts(accounts) {
return importAccounts({ accounts: normalAccounts });
}
export function importFetchedStatus(status) {
return importFetchedStatuses([status]);
export function importFetchedStatus(status, options = {}) {
return importFetchedStatuses([status], options);
}
export function importFetchedStatuses(statuses) {
export function importFetchedStatuses(statuses, options = {}) {
return (dispatch, getState) => {
const accounts = [];
const normalStatuses = [];
@@ -58,7 +58,7 @@ export function importFetchedStatuses(statuses) {
const filters = [];
function processStatus(status) {
pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id])));
pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id]), options));
pushUnique(accounts, status.account);
if (status.filtered) {

View File

@@ -27,9 +27,12 @@ function stripQuoteFallback(text) {
return wrapper.innerHTML;
}
export function normalizeStatus(status, normalOldStatus) {
export function normalizeStatus(status, normalOldStatus, { bogusQuotePolicy = false }) {
const normalStatus = { ...status };
if (bogusQuotePolicy)
normalStatus.quote_approval = null;
normalStatus.account = status.account.id;
if (status.reblog && status.reblog.id) {
@@ -109,6 +112,8 @@ export function normalizeStatus(status, normalOldStatus) {
}
if (normalOldStatus) {
normalStatus.quote_approval ||= normalOldStatus.quote_approval;
const list = normalOldStatus.get('media_attachments');
if (normalStatus.media_attachments && list) {
normalStatus.media_attachments.forEach(item => {

View File

@@ -203,8 +203,8 @@ export function deleteStatusFail(id, error) {
};
}
export const updateStatus = status => dispatch =>
dispatch(importFetchedStatus(status));
export const updateStatus = (status, { bogusQuotePolicy }) => dispatch =>
dispatch(importFetchedStatus(status, { bogusQuotePolicy }));
export function muteStatus(id) {
return (dispatch) => {

View File

@@ -52,6 +52,9 @@ const randomUpTo = max =>
export const connectTimelineStream = (timelineId, channelName, params = {}, options = {}) => {
const { messages } = getLocale();
// Public streams are currently not returning personalized quote policies
const bogusQuotePolicy = channelName.startsWith('public') || channelName.startsWith('hashtag');
return connectStream(channelName, params, (dispatch, getState) => {
// @ts-ignore
const locale = getState().getIn(['meta', 'locale']);
@@ -97,11 +100,11 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
switch (data.event) {
case 'update':
// @ts-expect-error
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), options.accept));
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), { accept: options.accept, bogusQuotePolicy }));
break;
case 'status.update':
// @ts-expect-error
dispatch(updateStatus(JSON.parse(data.payload)));
dispatch(updateStatus(JSON.parse(data.payload), { bogusQuotePolicy }));
break;
case 'delete':
dispatch(deleteFromTimelines(data.payload));

View File

@@ -32,7 +32,7 @@ export const loadPending = timeline => ({
timeline,
});
export function updateTimeline(timeline, status, accept) {
export function updateTimeline(timeline, status, { accept = undefined, bogusQuotePolicy = false } = {}) {
return (dispatch, getState) => {
if (typeof accept === 'function' && !accept(status)) {
return;
@@ -45,7 +45,7 @@ export function updateTimeline(timeline, status, accept) {
return;
}
dispatch(importFetchedStatus(status));
dispatch(importFetchedStatus(status, { bogusQuotePolicy }));
dispatch({
type: TIMELINE_UPDATE,

View File

@@ -26,6 +26,7 @@ import {
closeDropdownMenu,
} from 'mastodon/actions/dropdown_menu';
import { openModal, closeModal } from 'mastodon/actions/modal';
import { fetchStatus } from 'mastodon/actions/statuses';
import { CircularProgress } from 'mastodon/components/circular_progress';
import { isUserTouching } from 'mastodon/is_mobile';
import {
@@ -42,16 +43,10 @@ import { IconButton } from './icon_button';
let id = 0;
export interface RenderItemFnHandlers {
onClick: React.MouseEventHandler;
onKeyUp: React.KeyboardEventHandler;
}
export type RenderItemFn<Item = MenuItem> = (
item: Item,
index: number,
handlers: RenderItemFnHandlers,
focusRefCallback?: (c: HTMLAnchorElement | HTMLButtonElement | null) => void,
onClick: React.MouseEventHandler,
) => React.ReactNode;
type ItemClickFn<Item = MenuItem> = (item: Item, index: number) => void;
@@ -101,7 +96,6 @@ export const DropdownMenu = <Item = MenuItem,>({
onItemClick,
}: DropdownMenuProps<Item>) => {
const nodeRef = useRef<HTMLDivElement>(null);
const focusedItemRef = useRef<HTMLElement | null>(null);
useEffect(() => {
const handleDocumentClick = (e: MouseEvent) => {
@@ -163,8 +157,11 @@ export const DropdownMenu = <Item = MenuItem,>({
document.addEventListener('click', handleDocumentClick, { capture: true });
document.addEventListener('keydown', handleKeyDown, { capture: true });
if (focusedItemRef.current && openedViaKeyboard) {
focusedItemRef.current.focus({ preventScroll: true });
if (openedViaKeyboard) {
const firstMenuItem = nodeRef.current?.querySelector<
HTMLAnchorElement | HTMLButtonElement
>('li:first-child > :is(a, button)');
firstMenuItem?.focus({ preventScroll: true });
}
return () => {
@@ -175,13 +172,6 @@ export const DropdownMenu = <Item = MenuItem,>({
};
}, [onClose, openedViaKeyboard]);
const handleFocusedItemRef = useCallback(
(c: HTMLAnchorElement | HTMLButtonElement | null) => {
focusedItemRef.current = c as HTMLElement;
},
[],
);
const handleItemClick = useCallback(
(e: React.MouseEvent | React.KeyboardEvent) => {
const i = Number(e.currentTarget.getAttribute('data-index'));
@@ -207,15 +197,6 @@ export const DropdownMenu = <Item = MenuItem,>({
[onClose, onItemClick, items],
);
const handleItemKeyUp = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
handleItemClick(e);
}
},
[handleItemClick],
);
const nativeRenderItem = (option: Item, i: number) => {
if (!isMenuItem(option)) {
return null;
@@ -232,9 +213,7 @@ export const DropdownMenu = <Item = MenuItem,>({
if (isActionItem(option)) {
element = (
<button
ref={i === 0 ? handleFocusedItemRef : undefined}
onClick={handleItemClick}
onKeyUp={handleItemKeyUp}
data-index={i}
aria-disabled={disabled}
>
@@ -248,9 +227,7 @@ export const DropdownMenu = <Item = MenuItem,>({
target={option.target ?? '_target'}
data-method={option.method}
rel='noopener'
ref={i === 0 ? handleFocusedItemRef : undefined}
onClick={handleItemClick}
onKeyUp={handleItemKeyUp}
data-index={i}
>
<DropdownMenuItemContent item={option} />
@@ -258,13 +235,7 @@ export const DropdownMenu = <Item = MenuItem,>({
);
} else {
element = (
<Link
to={option.to}
ref={i === 0 ? handleFocusedItemRef : undefined}
onClick={handleItemClick}
onKeyUp={handleItemKeyUp}
data-index={i}
>
<Link to={option.to} onClick={handleItemClick} data-index={i}>
<DropdownMenuItemContent item={option} />
</Link>
);
@@ -307,15 +278,7 @@ export const DropdownMenu = <Item = MenuItem,>({
})}
>
{items.map((option, i) =>
renderItemMethod(
option,
i,
{
onClick: handleItemClick,
onKeyUp: handleItemKeyUp,
},
i === 0 ? handleFocusedItemRef : undefined,
),
renderItemMethod(option, i, handleItemClick),
)}
</ul>
)}
@@ -340,6 +303,7 @@ interface DropdownProps<Item extends object | null = MenuItem> {
*/
scrollKey?: string;
status?: ImmutableMap<string, unknown>;
needsStatusRefresh?: boolean;
forceDropdown?: boolean;
renderItem?: RenderItemFn<Item>;
renderHeader?: RenderHeaderFn<Item>;
@@ -363,6 +327,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
placement = 'bottom',
offset = [5, 5],
status,
needsStatusRefresh,
forceDropdown = false,
renderItem,
renderHeader,
@@ -382,6 +347,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
const prefetchAccountId = status
? status.getIn(['account', 'id'])
: undefined;
const statusId = status?.get('id') as string | undefined;
const handleClose = useCallback(() => {
if (buttonRef.current) {
@@ -399,7 +365,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
}, [dispatch, currentId]);
const handleItemClick = useCallback(
(e: React.MouseEvent | React.KeyboardEvent) => {
(e: React.MouseEvent) => {
const i = Number(e.currentTarget.getAttribute('data-index'));
const item = items?.[i];
@@ -420,10 +386,20 @@ export const Dropdown = <Item extends object | null = MenuItem>({
[handleClose, onItemClick, items],
);
const toggleDropdown = useCallback(
(e: React.MouseEvent | React.KeyboardEvent) => {
const { type } = e;
const isKeypressRef = useRef(false);
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
if (e.key === ' ' || e.key === 'Enter') {
isKeypressRef.current = true;
}
}, []);
const unsetIsKeypress = useCallback(() => {
isKeypressRef.current = false;
}, []);
const toggleDropdown = useCallback(
(e: React.MouseEvent) => {
if (open) {
handleClose();
} else {
@@ -436,6 +412,15 @@ export const Dropdown = <Item extends object | null = MenuItem>({
dispatch(fetchRelationships([prefetchAccountId]));
}
if (needsStatusRefresh && statusId) {
dispatch(
fetchStatus(statusId, {
forceFetch: true,
alsoFetchContext: false,
}),
);
}
if (isUserTouching() && !forceDropdown) {
dispatch(
openModal({
@@ -450,10 +435,11 @@ export const Dropdown = <Item extends object | null = MenuItem>({
dispatch(
openDropdownMenu({
id: currentId,
keyboard: type !== 'click',
keyboard: isKeypressRef.current,
scrollKey,
}),
);
isKeypressRef.current = false;
}
}
},
@@ -468,6 +454,8 @@ export const Dropdown = <Item extends object | null = MenuItem>({
items,
forceDropdown,
handleClose,
statusId,
needsStatusRefresh,
],
);
@@ -484,6 +472,9 @@ export const Dropdown = <Item extends object | null = MenuItem>({
const buttonProps = {
disabled,
onClick: toggleDropdown,
onKeyDown: handleKeyDown,
onKeyUp: unsetIsKeypress,
onBlur: unsetIsKeypress,
'aria-expanded': open,
'aria-controls': menuId,
ref: buttonRef,

View File

@@ -58,17 +58,7 @@ export const EditedTimestamp: React.FC<{
}, []);
const renderItem = useCallback(
(
item: HistoryItem,
index: number,
{
onClick,
onKeyUp,
}: {
onClick: React.MouseEventHandler;
onKeyUp: React.KeyboardEventHandler;
},
) => {
(item: HistoryItem, index: number, onClick: React.MouseEventHandler) => {
const formattedDate = (
<RelativeTimestamp
timestamp={item.get('created_at') as string}
@@ -98,7 +88,7 @@ export const EditedTimestamp: React.FC<{
className='dropdown-menu__item edited-timestamp__history__item'
key={item.get('created_at') as string}
>
<button data-index={index} onClick={onClick} onKeyUp={onKeyUp}>
<button data-index={index} onClick={onClick} type='button'>
{label}
</button>
</li>

View File

@@ -8,13 +8,14 @@ import classNames from 'classnames';
import { quoteComposeById } from '@/mastodon/actions/compose_typed';
import { toggleReblog } from '@/mastodon/actions/interactions';
import { openModal } from '@/mastodon/actions/modal';
import { fetchStatus } from '@/mastodon/actions/statuses';
import { quickBoosting } from '@/mastodon/initial_state';
import type { ActionMenuItem } from '@/mastodon/models/dropdown_menu';
import type { Status } from '@/mastodon/models/status';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import type { SomeRequired } from '@/mastodon/utils/types';
import type { RenderItemFn, RenderItemFnHandlers } from '../dropdown_menu';
import type { RenderItemFn } from '../dropdown_menu';
import { Dropdown, DropdownMenuItemContent } from '../dropdown_menu';
import { IconButton } from '../icon_button';
@@ -74,18 +75,12 @@ const StandaloneBoostButton: FC<ReblogButtonProps> = ({ status, counters }) => {
);
};
const renderMenuItem: RenderItemFn<ActionMenuItem> = (
item,
index,
handlers,
focusRefCallback,
) => (
const renderMenuItem: RenderItemFn<ActionMenuItem> = (item, index, onClick) => (
<ReblogMenuItem
index={index}
item={item}
handlers={handlers}
onClick={onClick}
key={`${item.text}-${index}`}
focusRefCallback={focusRefCallback}
/>
);
@@ -117,6 +112,7 @@ const BoostOrQuoteMenu: FC<ReblogButtonProps> = ({ status, counters }) => {
const statusId = status.get('id') as string;
const wasBoosted = !!status.get('reblogged');
const quoteApproval = status.get('quote_approval');
const showLoginPrompt = useCallback(() => {
dispatch(
@@ -173,9 +169,16 @@ const BoostOrQuoteMenu: FC<ReblogButtonProps> = ({ status, counters }) => {
dispatch(toggleReblog(status.get('id'), true));
return false;
}
if (quoteApproval === null) {
dispatch(
fetchStatus(statusId, { forceFetch: true, alsoFetchContext: false }),
);
}
return true;
},
[dispatch, isLoggedIn, showLoginPrompt, status],
[dispatch, isLoggedIn, showLoginPrompt, status, quoteApproval, statusId],
);
return (
@@ -208,16 +211,10 @@ const BoostOrQuoteMenu: FC<ReblogButtonProps> = ({ status, counters }) => {
interface ReblogMenuItemProps {
item: ActionMenuItem;
index: number;
handlers: RenderItemFnHandlers;
focusRefCallback?: (c: HTMLAnchorElement | HTMLButtonElement | null) => void;
onClick: React.MouseEventHandler;
}
const ReblogMenuItem: FC<ReblogMenuItemProps> = ({
index,
item,
handlers,
focusRefCallback,
}) => {
const ReblogMenuItem: FC<ReblogMenuItemProps> = ({ index, item, onClick }) => {
const { text, highlighted, disabled } = item;
return (
@@ -227,12 +224,7 @@ const ReblogMenuItem: FC<ReblogMenuItemProps> = ({
})}
key={`${text}-${index}`}
>
<button
{...handlers}
ref={focusRefCallback}
aria-disabled={disabled}
data-index={index}
>
<button onClick={onClick} aria-disabled={disabled} data-index={index}>
<DropdownMenuItemContent item={item} />
</button>
</li>

View File

@@ -404,6 +404,7 @@ class StatusActionBar extends ImmutablePureComponent {
<Dropdown
scrollKey={scrollKey}
status={status}
needsStatusRefresh={quickBoosting && status.get('quote_approval') === null}
items={menu}
icon='ellipsis-h'
iconComponent={MoreHorizIcon}

View File

@@ -49,6 +49,7 @@ export const StatusBanner: React.FC<{
<button
ref={buttonRef}
type='button'
className='link-button'
onClick={onClick}
aria-describedby={descriptionId}

View File

@@ -32,16 +32,38 @@ interface Rule extends BaseRule {
translations?: Record<string, BaseRule>;
}
function getDefaultSelectedLocale(
currentUiLocale: string,
localeOptions: SelectItem[],
) {
const preciseMatch = localeOptions.find(
(option) => option.value === currentUiLocale,
);
if (preciseMatch) {
return preciseMatch.value;
}
const partialLocale = currentUiLocale.split('-')[0];
const partialMatch = localeOptions.find(
(option) => option.value.split('-')[0] === partialLocale,
);
return partialMatch?.value ?? 'default';
}
export const RulesSection: FC<RulesSectionProps> = ({ isLoading = false }) => {
const intl = useIntl();
const [locale, setLocale] = useState(intl.locale);
const rules = useAppSelector((state) => rulesSelector(state, locale));
const localeOptions = useAppSelector((state) =>
localeOptionsSelector(state, intl),
);
const [selectedLocale, setSelectedLocale] = useState(() =>
getDefaultSelectedLocale(intl.locale, localeOptions),
);
const rules = useAppSelector((state) => rulesSelector(state, selectedLocale));
const handleLocaleChange: ChangeEventHandler<HTMLSelectElement> = useCallback(
(e) => {
setLocale(e.currentTarget.value);
setSelectedLocale(e.currentTarget.value);
},
[],
);
@@ -74,25 +96,27 @@ export const RulesSection: FC<RulesSectionProps> = ({ isLoading = false }) => {
))}
</ol>
<div className='rules-languages'>
<label htmlFor='language-select'>
<FormattedMessage
id='about.language_label'
defaultMessage='Language'
/>
</label>
<select onChange={handleLocaleChange} id='language-select'>
{localeOptions.map((option) => (
<option
key={option.value}
value={option.value}
selected={option.value === locale}
>
{option.text}
</option>
))}
</select>
</div>
{localeOptions.length > 1 && (
<div className='rules-languages'>
<label htmlFor='language-select'>
<FormattedMessage
id='about.language_label'
defaultMessage='Language'
/>
</label>
<select onChange={handleLocaleChange} id='language-select'>
{localeOptions.map((option) => (
<option
key={option.value}
value={option.value}
selected={option.value === selectedLocale}
>
{option.text}
</option>
))}
</select>
</div>
)}
</Section>
);
};
@@ -145,9 +169,13 @@ const localeOptionsSelector = createSelector(
},
};
// Use the default locale as a target to translate language names.
const intlLocale = new Intl.DisplayNames(intl.locale, {
type: 'language',
});
const intlLocale =
// Intl.DisplayNames can be undefined in old browsers
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
Intl.DisplayNames &&
(new Intl.DisplayNames(intl.locale, {
type: 'language',
}) as Intl.DisplayNames | undefined);
for (const { translations } of rules) {
for (const locale in translations) {
if (langs[locale]) {
@@ -155,7 +183,7 @@ const localeOptionsSelector = createSelector(
}
langs[locale] = {
value: locale,
text: intlLocale.of(locale) ?? locale,
text: intlLocale?.of(locale) ?? locale,
};
}
}

View File

@@ -330,7 +330,7 @@ export const AltTextModal = forwardRef<ModalRef, Props & Partial<RestoreProps>>(
});
}, [dispatch, setIsSaving, mediaId, onClose, position, description]);
const handleKeyUp = useCallback(
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
@@ -457,7 +457,7 @@ export const AltTextModal = forwardRef<ModalRef, Props & Partial<RestoreProps>>(
id='description'
value={isDetecting ? ' ' : description}
onChange={handleDescriptionChange}
onKeyUp={handleKeyUp}
onKeyDown={handleKeyDown}
lang={lang}
placeholder={intl.formatMessage(
type === 'audio'

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef, useCallback, useState, useId } from 'react';
import { useEffect, useRef, useCallback, useState } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
@@ -22,6 +22,8 @@ import { useAudioVisualizer } from 'mastodon/hooks/useAudioVisualizer';
import { displayMedia, useBlurhash } from 'mastodon/initial_state';
import { playerSettings } from 'mastodon/settings';
import { AudioVisualizer } from './visualizer';
const messages = defineMessages({
play: { id: 'video.play', defaultMessage: 'Play' },
pause: { id: 'video.pause', defaultMessage: 'Pause' },
@@ -116,7 +118,6 @@ export const Audio: React.FC<{
const seekRef = useRef<HTMLDivElement>(null);
const volumeRef = useRef<HTMLDivElement>(null);
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>();
const accessibilityId = useId();
const { audioContextRef, sourceRef, gainNodeRef, playAudio, pauseAudio } =
useAudioContext({ audioElementRef: audioRef });
@@ -538,19 +539,6 @@ export const Audio: React.FC<{
[togglePlay, toggleMute],
);
const springForBand0 = useSpring({
to: { r: 50 + (frequencyBands[0] ?? 0) * 10 },
config: config.wobbly,
});
const springForBand1 = useSpring({
to: { r: 50 + (frequencyBands[1] ?? 0) * 10 },
config: config.wobbly,
});
const springForBand2 = useSpring({
to: { r: 50 + (frequencyBands[2] ?? 0) * 10 },
config: config.wobbly,
});
const progress = Math.min((currentTime / loadedDuration) * 100, 100);
const effectivelyMuted = muted || volume === 0;
@@ -641,81 +629,7 @@ export const Audio: React.FC<{
</div>
<div className='audio-player__controls__play'>
<svg
className='audio-player__visualizer'
viewBox='0 0 124 124'
xmlns='http://www.w3.org/2000/svg'
>
<animated.circle
opacity={0.5}
cx={57}
cy={62.5}
r={springForBand0.r}
fill='var(--player-accent-color)'
/>
<animated.circle
opacity={0.5}
cx={65}
cy={57.5}
r={springForBand1.r}
fill='var(--player-accent-color)'
/>
<animated.circle
opacity={0.5}
cx={63}
cy={66.5}
r={springForBand2.r}
fill='var(--player-accent-color)'
/>
<g clipPath={`url(#${accessibilityId}-clip)`}>
<rect
x={14}
y={14}
width={96}
height={96}
fill={`url(#${accessibilityId}-pattern)`}
/>
<rect
x={14}
y={14}
width={96}
height={96}
fill='var(--player-background-color'
opacity={0.45}
/>
</g>
<defs>
<pattern
id={`${accessibilityId}-pattern`}
patternContentUnits='objectBoundingBox'
width='1'
height='1'
>
<use href={`#${accessibilityId}-image`} />
</pattern>
<clipPath id={`${accessibilityId}-clip`}>
<rect
x={14}
y={14}
width={96}
height={96}
rx={48}
fill='white'
/>
</clipPath>
<image
id={`${accessibilityId}-image`}
href={poster}
width={1}
height={1}
preserveAspectRatio='none'
/>
</defs>
</svg>
<AudioVisualizer frequencyBands={frequencyBands} poster={poster} />
<button
type='button'

View File

@@ -0,0 +1,100 @@
import { useId } from 'react';
import type { FC } from 'react';
import { animated, config, useSpring } from '@react-spring/web';
interface AudioVisualizerProps {
frequencyBands?: number[];
poster?: string;
}
export const AudioVisualizer: FC<AudioVisualizerProps> = ({
frequencyBands = [],
poster,
}) => {
const accessibilityId = useId();
const springForBand0 = useSpring({
to: { r: 50 + (frequencyBands[0] ?? 0) * 10 },
config: config.wobbly,
});
const springForBand1 = useSpring({
to: { r: 50 + (frequencyBands[1] ?? 0) * 10 },
config: config.wobbly,
});
const springForBand2 = useSpring({
to: { r: 50 + (frequencyBands[2] ?? 0) * 10 },
config: config.wobbly,
});
return (
<svg
className='audio-player__visualizer'
viewBox='0 0 124 124'
xmlns='http://www.w3.org/2000/svg'
>
<animated.circle
opacity={0.5}
cx={57}
cy={62.5}
r={springForBand0.r}
fill='var(--player-accent-color)'
/>
<animated.circle
opacity={0.5}
cx={65}
cy={57.5}
r={springForBand1.r}
fill='var(--player-accent-color)'
/>
<animated.circle
opacity={0.5}
cx={63}
cy={66.5}
r={springForBand2.r}
fill='var(--player-accent-color)'
/>
<g clipPath={`url(#${accessibilityId}-clip)`}>
<rect
x={14}
y={14}
width={96}
height={96}
fill={`url(#${accessibilityId}-pattern)`}
/>
<rect
x={14}
y={14}
width={96}
height={96}
fill='var(--player-background-color'
opacity={0.45}
/>
</g>
<defs>
<pattern
id={`${accessibilityId}-pattern`}
patternContentUnits='objectBoundingBox'
width='1'
height='1'
>
<use href={`#${accessibilityId}-image`} />
</pattern>
<clipPath id={`${accessibilityId}-clip`}>
<rect x={14} y={14} width={96} height={96} rx={48} fill='white' />
</clipPath>
<image
id={`${accessibilityId}-image`}
href={poster}
width={1}
height={1}
preserveAspectRatio='none'
/>
</defs>
</svg>
);
};

View File

@@ -123,11 +123,10 @@ class ComposeForm extends ImmutablePureComponent {
};
canSubmit = () => {
const { isSubmitting, isChangingUpload, isUploading, anyMedia, maxChars } = this.props;
const { isSubmitting, isChangingUpload, isUploading, maxChars } = this.props;
const fulltext = this.getFulltextForCharacterCounting();
const isOnlyWhitespace = fulltext.length !== 0 && fulltext.trim().length === 0;
return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > maxChars || (isOnlyWhitespace && !anyMedia));
return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > maxChars);
};
handleSubmit = (e) => {
@@ -141,7 +140,10 @@ class ComposeForm extends ImmutablePureComponent {
return;
}
this.props.onSubmit(missingAltTextModal && this.props.missingAltText && this.props.privacy !== 'direct');
this.props.onSubmit({
missingAltText: missingAltTextModal && this.props.missingAltText && this.props.privacy !== 'direct',
quoteToPrivate: this.props.quoteToPrivate,
});
if (e) {
e.preventDefault();

View File

@@ -0,0 +1,48 @@
import { useCallback } from 'react';
import type { FC } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { cancelPasteLinkCompose } from '@/mastodon/actions/compose_typed';
import { useAppDispatch } from '@/mastodon/store';
import CancelFillIcon from '@/material-icons/400-24px/cancel-fill.svg?react';
import { DisplayName } from 'mastodon/components/display_name';
import { IconButton } from 'mastodon/components/icon_button';
import { Skeleton } from 'mastodon/components/skeleton';
const messages = defineMessages({
quote_cancel: { id: 'status.quote.cancel', defaultMessage: 'Cancel quote' },
});
export const QuotePlaceholder: FC = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const handleQuoteCancel = useCallback(() => {
dispatch(cancelPasteLinkCompose());
}, [dispatch]);
return (
<div className='status__quote'>
<div className='status'>
<div className='status__info'>
<div className='status__avatar'>
<Skeleton width='32px' height='32px' />
</div>
<div className='status__display-name'>
<DisplayName />
</div>
<IconButton
onClick={handleQuoteCancel}
className='status__quote-cancel'
title={intl.formatMessage(messages.quote_cancel)}
icon='cancel-fill'
iconComponent={CancelFillIcon}
/>
</div>
<div className='status__content'>
<Skeleton />
</div>
</div>
</div>
);
};

View File

@@ -7,11 +7,17 @@ import { quoteComposeCancel } from '@/mastodon/actions/compose_typed';
import { QuotedStatus } from '@/mastodon/components/status_quoted';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import { QuotePlaceholder } from './quote_placeholder';
export const ComposeQuotedStatus: FC = () => {
const quotedStatusId = useAppSelector(
(state) => state.compose.get('quoted_status_id') as string | null,
);
const isFetchingLink = useAppSelector(
(state) => !!state.compose.get('fetching_link'),
);
const isEditing = useAppSelector((state) => !!state.compose.get('id'));
const quote = useMemo(
@@ -30,7 +36,9 @@ export const ComposeQuotedStatus: FC = () => {
dispatch(quoteComposeCancel());
}, [dispatch]);
if (!quote) {
if (isFetchingLink && !quote) {
return <QuotePlaceholder />;
} else if (!quote) {
return null;
}

View File

@@ -10,6 +10,7 @@ import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import CloseIcon from '@/material-icons/400-20px/close.svg?react';
import SoundIcon from '@/material-icons/400-24px/audio.svg?react';
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
import WarningIcon from '@/material-icons/400-24px/warning.svg?react';
import { undoUploadCompose } from 'mastodon/actions/compose';
@@ -17,7 +18,18 @@ import { openModal } from 'mastodon/actions/modal';
import { Blurhash } from 'mastodon/components/blurhash';
import { Icon } from 'mastodon/components/icon';
import type { MediaAttachment } from 'mastodon/models/media_attachment';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
import {
createAppSelector,
useAppDispatch,
useAppSelector,
} from 'mastodon/store';
import { AudioVisualizer } from '../../audio/visualizer';
const selectUserAvatar = createAppSelector(
[(state) => state.accounts, (state) => state.meta.get('me') as string],
(accounts, myId) => accounts.get(myId)?.avatar_static,
);
export const Upload: React.FC<{
id: string;
@@ -38,6 +50,7 @@ export const Upload: React.FC<{
const sensitive = useAppSelector(
(state) => state.compose.get('spoiler') as boolean,
);
const userAvatar = useAppSelector(selectUserAvatar);
const handleUndoClick = useCallback(() => {
dispatch(undoUploadCompose(id));
@@ -67,6 +80,8 @@ export const Upload: React.FC<{
transform: CSS.Transform.toString(transform),
transition,
};
const preview_url = media.get('preview_url') as string | null;
const blurhash = media.get('blurhash') as string | null;
return (
<div
@@ -85,17 +100,19 @@ export const Upload: React.FC<{
<div
className='compose-form__upload__thumbnail'
style={{
backgroundImage: !sensitive
? `url(${media.get('preview_url') as string})`
: undefined,
backgroundImage:
!sensitive && preview_url ? `url(${preview_url})` : undefined,
backgroundPosition: `${x}% ${y}%`,
}}
>
{sensitive && (
<Blurhash
hash={media.get('blurhash') as string}
className='compose-form__upload__preview'
/>
{sensitive && blurhash && (
<Blurhash hash={blurhash} className='compose-form__upload__preview' />
)}
{!sensitive && !preview_url && (
<div className='compose-form__upload__visualizer'>
<AudioVisualizer poster={userAvatar} />
<Icon id='sound' icon={SoundIcon} />
</div>
)}
<div className='compose-form__upload__actions'>

View File

@@ -5,8 +5,10 @@ import { defineMessages, useIntl } from 'react-intl';
import classNames from 'classnames';
import { changeComposeVisibility } from '@/mastodon/actions/compose';
import { setComposeQuotePolicy } from '@/mastodon/actions/compose_typed';
import {
changeComposeVisibility,
setComposeQuotePolicy,
} from '@/mastodon/actions/compose_typed';
import { openModal } from '@/mastodon/actions/modal';
import type { ApiQuotePolicy } from '@/mastodon/api_types/quotes';
import type { StatusVisibility } from '@/mastodon/api_types/statuses';

View File

@@ -12,6 +12,7 @@ import {
} from 'mastodon/actions/compose';
import { pasteLinkCompose } from 'mastodon/actions/compose_typed';
import { openModal } from 'mastodon/actions/modal';
import { PRIVATE_QUOTE_MODAL_ID } from 'mastodon/features/ui/components/confirmation_modals/private_quote_notify';
import ComposeForm from '../components/compose_form';
@@ -32,6 +33,10 @@ const mapStateToProps = state => ({
isUploading: state.getIn(['compose', 'is_uploading']),
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
missingAltText: state.getIn(['compose', 'media_attachments']).some(media => ['image', 'gifv'].includes(media.get('type')) && (media.get('description') ?? '').length === 0),
quoteToPrivate:
!!state.getIn(['compose', 'quoted_status_id'])
&& state.getIn(['compose', 'privacy']) === 'private'
&& !state.getIn(['settings', 'dismissed_banners', PRIVATE_QUOTE_MODAL_ID]),
isInReply: state.getIn(['compose', 'in_reply_to']) !== null,
lang: state.getIn(['compose', 'language']),
maxChars: state.getIn(['server', 'server', 'configuration', 'statuses', 'max_characters'], 500),
@@ -43,12 +48,17 @@ const mapDispatchToProps = (dispatch, props) => ({
dispatch(changeCompose(text));
},
onSubmit (missingAltText) {
onSubmit ({ missingAltText, quoteToPrivate }) {
if (missingAltText) {
dispatch(openModal({
modalType: 'CONFIRM_MISSING_ALT_TEXT',
modalProps: {},
}));
} else if (quoteToPrivate) {
dispatch(openModal({
modalType: 'CONFIRM_PRIVATE_QUOTE_NOTIFY',
modalProps: {},
}));
} else {
dispatch(submitCompose((status) => {
if (props.redirectOnSuccess) {

View File

@@ -1,8 +1,7 @@
import { connect } from 'react-redux';
import { changeComposeVisibility } from '../../../actions/compose';
import { openModal, closeModal } from '../../../actions/modal';
import { isUserTouching } from '../../../is_mobile';
import { changeComposeVisibility } from '@/mastodon/actions/compose_typed';
import PrivacyDropdown from '../components/privacy_dropdown';
const mapStateToProps = state => ({

View File

@@ -1,5 +1,5 @@
import { flattenEmojiData } from 'emojibase';
import type { CompactEmoji, FlatCompactEmoji } from 'emojibase';
import type { CompactEmoji, FlatCompactEmoji, Locale } from 'emojibase';
import {
putEmojiData,
@@ -43,9 +43,8 @@ async function fetchAndCheckEtag<ResultType extends object[]>(
if (locale === 'custom') {
url.pathname = '/api/v1/custom_emojis';
} else {
// This doesn't use isDevelopment() as that module loads initial state
// which breaks workers, as they cannot access the DOM.
url.pathname = `/packs${import.meta.env.DEV ? '-dev' : ''}/emoji/${locale}.json`;
const modulePath = await localeToPath(locale);
url.pathname = modulePath;
}
const oldEtag = await loadLatestEtag(locale);
@@ -80,3 +79,19 @@ async function fetchAndCheckEtag<ResultType extends object[]>(
return data;
}
const modules = import.meta.glob<string>(
'../../../../../node_modules/emojibase-data/**/compact.json',
{
query: '?url',
import: 'default',
},
);
function localeToPath(locale: Locale) {
const key = `../../../../../node_modules/emojibase-data/${locale}/compact.json`;
if (!modules[key] || typeof modules[key] !== 'function') {
throw new Error(`Unsupported locale: ${locale}`);
}
return modules[key]();
}

View File

@@ -1,11 +1,13 @@
import { useEffect, useState } from 'react';
import type { FC } from 'react';
import { useEffect, useState } from 'react';
import { FormattedDate, FormattedMessage } from 'react-intl';
import { dismissAnnouncement } from '@/mastodon/actions/announcements';
import type { ApiAnnouncementJSON } from '@/mastodon/api_types/announcements';
import { AnimateEmojiProvider } from '@/mastodon/components/emoji/context';
import { EmojiHTML } from '@/mastodon/components/emoji/html';
import { useAppDispatch } from '@/mastodon/store';
import { ReactionsBar } from './reactions';
@@ -22,13 +24,23 @@ export const Announcement: FC<AnnouncementProps> = ({
announcement,
selected,
}) => {
const [unread, setUnread] = useState(!announcement.read);
const { read, id } = announcement;
// Dismiss announcement when it becomes active.
const dispatch = useAppDispatch();
useEffect(() => {
// Only update `unread` marker once the announcement is out of view
if (!selected && unread !== !announcement.read) {
setUnread(!announcement.read);
if (selected && !read) {
dispatch(dismissAnnouncement(id));
}
}, [announcement.read, selected, unread]);
}, [selected, id, dispatch, read]);
// But visually show the announcement as read only when it goes out of view.
const [unread, setUnread] = useState(!read);
useEffect(() => {
if (!selected && unread !== !read) {
setUnread(!read);
}
}, [selected, unread, read]);
return (
<AnimateEmojiProvider className='announcements__item'>

View File

@@ -417,6 +417,7 @@ export const DetailedStatus: React.FC<{
<QuotedStatus
quote={status.get('quote')}
parentQuotePostId={status.get('id')}
contextType='thread'
/>
)}
</>

View File

@@ -295,7 +295,7 @@ export const RefreshController: React.FC<{
if (loadingState === 'loading') {
return (
<div
className='load-more load-gap'
className='load-more load-more--large'
aria-busy
aria-live='polite'
aria-label={intl.formatMessage(messages.loadingInitial)}

View File

@@ -159,7 +159,7 @@ class Status extends ImmutablePureComponent {
};
UNSAFE_componentWillMount () {
this.props.dispatch(fetchStatus(this.props.params.statusId));
this.props.dispatch(fetchStatus(this.props.params.statusId, { forceFetch: true }));
}
componentDidMount () {
@@ -170,7 +170,7 @@ class Status extends ImmutablePureComponent {
UNSAFE_componentWillReceiveProps (nextProps) {
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
this.props.dispatch(fetchStatus(nextProps.params.statusId));
this.props.dispatch(fetchStatus(nextProps.params.statusId, { forceFetch: true }));
}
if (nextProps.status && nextProps.status.get('id') !== this.state.loadedStatusId) {
@@ -299,6 +299,12 @@ class Status extends ImmutablePureComponent {
dispatch(openModal({ modalType: 'COMPOSE_PRIVACY', modalProps: { statusId, onChange: handleChange } }));
};
handleQuote = (status) => {
const { dispatch } = this.props;
dispatch(quoteComposeById(status.get('id')));
};
handleEditClick = (status) => {
const { dispatch, askReplyConfirmation } = this.props;
@@ -625,6 +631,7 @@ class Status extends ImmutablePureComponent {
onDelete={this.handleDeleteClick}
onRevokeQuote={this.handleRevokeQuoteClick}
onQuotePolicyChange={this.handleQuotePolicyChange}
onQuote={this.handleQuote}
onEdit={this.handleEditClick}
onDirect={this.handleDirectClick}
onMention={this.handleMentionClick}

View File

@@ -18,6 +18,7 @@ export const ConfirmationModal: React.FC<
onSecondary?: () => void;
onConfirm: () => void;
closeWhenConfirm?: boolean;
extraContent?: React.ReactNode;
} & BaseConfirmationModalProps
> = ({
title,
@@ -29,6 +30,7 @@ export const ConfirmationModal: React.FC<
secondary,
onSecondary,
closeWhenConfirm = true,
extraContent,
}) => {
const handleClick = useCallback(() => {
if (closeWhenConfirm) {
@@ -49,6 +51,8 @@ export const ConfirmationModal: React.FC<
<div className='safety-action-modal__confirmation'>
<h1>{title}</h1>
{message && <p>{message}</p>}
{extraContent}
</div>
</div>

View File

@@ -0,0 +1,88 @@
import { forwardRef, useCallback, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { submitCompose } from '@/mastodon/actions/compose';
import { changeSetting } from '@/mastodon/actions/settings';
import { CheckBox } from '@/mastodon/components/check_box';
import { useAppDispatch } from '@/mastodon/store';
import { ConfirmationModal } from './confirmation_modal';
import type { BaseConfirmationModalProps } from './confirmation_modal';
import classes from './styles.module.css';
export const PRIVATE_QUOTE_MODAL_ID = 'quote/private_notify';
const messages = defineMessages({
title: {
id: 'confirmations.private_quote_notify.title',
defaultMessage: 'Share with followers and mentioned users?',
},
message: {
id: 'confirmations.private_quote_notify.message',
defaultMessage:
'The person you are quoting and other mentions ' +
"will be notified and will be able to view your post, even if they're not following you.",
},
confirm: {
id: 'confirmations.private_quote_notify.confirm',
defaultMessage: 'Publish post',
},
cancel: {
id: 'confirmations.private_quote_notify.cancel',
defaultMessage: 'Back to editing',
},
});
export const PrivateQuoteNotify = forwardRef<
HTMLDivElement,
BaseConfirmationModalProps
>(
(
{ onClose },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_ref,
) => {
const intl = useIntl();
const [dismiss, setDismissed] = useState(false);
const handleDismissToggle = useCallback(() => {
setDismissed((prev) => !prev);
}, []);
const dispatch = useAppDispatch();
const handleConfirm = useCallback(() => {
dispatch(submitCompose());
if (dismiss) {
dispatch(
changeSetting(['dismissed_banners', PRIVATE_QUOTE_MODAL_ID], true),
);
}
}, [dismiss, dispatch]);
return (
<ConfirmationModal
title={intl.formatMessage(messages.title)}
message={intl.formatMessage(messages.message)}
confirm={intl.formatMessage(messages.confirm)}
cancel={intl.formatMessage(messages.cancel)}
onConfirm={handleConfirm}
onClose={onClose}
extraContent={
<label className={classes.checkbox_wrapper}>
<CheckBox
value='hide'
checked={dismiss}
onChange={handleDismissToggle}
/>{' '}
<FormattedMessage
id='confirmations.private_quote_notify.do_not_show_again'
defaultMessage="Don't show me this message again"
/>
</label>
}
/>
);
},
);
PrivateQuoteNotify.displayName = 'PrivateQuoteNotify';

View File

@@ -0,0 +1,7 @@
.checkbox_wrapper {
display: flex;
align-items: center;
gap: 0.5rem;
margin: 1rem 0;
cursor: pointer;
}

View File

@@ -47,6 +47,7 @@ import MediaModal from './media_modal';
import { ModalPlaceholder } from './modal_placeholder';
import VideoModal from './video_modal';
import { VisibilityModal } from './visibility_modal';
import { PrivateQuoteNotify } from './confirmation_modals/private_quote_notify';
export const MODAL_COMPONENTS = {
'MEDIA': () => Promise.resolve({ default: MediaModal }),
@@ -66,6 +67,7 @@ export const MODAL_COMPONENTS = {
'CONFIRM_LOG_OUT': () => Promise.resolve({ default: ConfirmLogOutModal }),
'CONFIRM_FOLLOW_TO_LIST': () => Promise.resolve({ default: ConfirmFollowToListModal }),
'CONFIRM_MISSING_ALT_TEXT': () => Promise.resolve({ default: ConfirmMissingAltTextModal }),
'CONFIRM_PRIVATE_QUOTE_NOTIFY': () => Promise.resolve({ default: PrivateQuoteNotify }),
'CONFIRM_REVOKE_QUOTE': () => Promise.resolve({ default: ConfirmRevokeQuoteModal }),
'CONFIRM_QUIET_QUOTE': () => Promise.resolve({ default: QuietPostQuoteInfoModal }),
'MUTE': MuteModal,

View File

@@ -128,9 +128,12 @@ export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
const disableVisibility = !!statusId;
const disableQuotePolicy =
visibility === 'private' || visibility === 'direct';
const disablePublicVisibilities: boolean = useAppSelector(
const disablePublicVisibilities = useAppSelector(
selectDisablePublicVisibilities,
);
const isQuotePost = useAppSelector(
(state) => state.compose.get('quoted_status_id') !== null,
);
const visibilityItems = useMemo<SelectItem<StatusVisibility>[]>(() => {
const items: SelectItem<StatusVisibility>[] = [
@@ -315,6 +318,21 @@ export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
id={quoteDescriptionId}
/>
</div>
{isQuotePost && visibility === 'direct' && (
<div className='visibility-modal__quote-warning'>
<FormattedMessage
id='visibility_modal.direct_quote_warning.title'
defaultMessage="Quotes can't be embedded in private mentions"
tagName='h3'
/>
<FormattedMessage
id='visibility_modal.direct_quote_warning.text'
defaultMessage='If you save the current settings, the embedded quote will be converted to a link.'
tagName='p'
/>
</div>
)}
</div>
<div className='dialog-modal__content__actions'>
<Button onClick={onClose} secondary>

View File

@@ -35,7 +35,7 @@ interface InitialStateMeta {
streaming_api_base_url: string;
local_live_feed_access: 'public' | 'authenticated' | 'disabled';
remote_live_feed_access: 'public' | 'authenticated' | 'disabled';
local_topic_feed_access: 'public' | 'authenticated' | 'disabled';
local_topic_feed_access: 'public' | 'authenticated';
remote_topic_feed_access: 'public' | 'authenticated' | 'disabled';
title: string;
show_trends: boolean;
@@ -129,17 +129,21 @@ export const statusPageUrl = getMeta('status_page_url');
export const sso_redirect = getMeta('sso_redirect');
export const termsOfServiceEnabled = getMeta('terms_of_service_enabled');
const displayNames = new Intl.DisplayNames(getMeta('locale'), {
type: 'language',
fallback: 'none',
languageDisplay: 'standard',
});
const displayNames =
// Intl.DisplayNames can be undefined in old browsers
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
Intl.DisplayNames &&
(new Intl.DisplayNames(getMeta('locale'), {
type: 'language',
fallback: 'none',
languageDisplay: 'standard',
}) as Intl.DisplayNames | undefined);
export const languages = initialState?.languages.map((lang) => {
// zh-YUE is not a valid CLDR unicode_language_id
return [
lang[0],
displayNames.of(lang[0].replace('zh-YUE', 'yue')) ?? lang[1],
displayNames?.of(lang[0].replace('zh-YUE', 'yue')) ?? lang[1],
lang[2],
];
});

View File

@@ -24,7 +24,7 @@
"account.blocking": "Блакіраванне",
"account.cancel_follow_request": "Скасаваць запыт на падпіску",
"account.copy": "Скапіраваць спасылку на профіль",
"account.direct": "Згадаць асабіста @{name}",
"account.direct": "Згадаць прыватна @{name}",
"account.disable_notifications": "Не паведамляць мне пра публікацыі @{name}",
"account.domain_blocking": "Блакіраванне дамена",
"account.edit_profile": "Рэдагаваць профіль",
@@ -194,6 +194,7 @@
"community.column_settings.local_only": "Толькі лакальныя",
"community.column_settings.media_only": "Толькі медыя",
"community.column_settings.remote_only": "Толькі дыстанцыйна",
"compose.error.blank_post": "Допіс не можа быць пустым.",
"compose.language.change": "Змяніць мову",
"compose.language.search": "Шукаць мовы...",
"compose.published.body": "Допіс апублікаваны.",
@@ -246,6 +247,11 @@
"confirmations.missing_alt_text.secondary": "Усё адно апублікаваць",
"confirmations.missing_alt_text.title": "Дадаць альтэрнатыўны тэкст?",
"confirmations.mute.confirm": "Ігнараваць",
"confirmations.private_quote_notify.cancel": "Звяртацца да рэдагавання",
"confirmations.private_quote_notify.confirm": "Апублікаваць допіс",
"confirmations.private_quote_notify.do_not_show_again": "Больш не паказваць мне гэтае паведамленне",
"confirmations.private_quote_notify.message": "Асоба, якую Вы цытуеце, і іншыя, хто быў узгаданы, атрымаюць апавяшчэнні і змогуць пабачыць Ваш допіс, нават калі яны не падпісаныя на Вас.",
"confirmations.private_quote_notify.title": "Падзяліцца з падпісчыкамі і ўзгаданымі карыстальнікамі?",
"confirmations.quiet_post_quote_info.dismiss": "Не нагадваць зноў",
"confirmations.quiet_post_quote_info.got_it": "Зразумела",
"confirmations.quiet_post_quote_info.message": "Калі будзеце цытаваць ціхі публічны допіс, Ваш допіс будзе схаваны ад трэндавых стужак.",
@@ -758,6 +764,7 @@
"privacy_policy.title": "Палітыка канфідэнцыйнасці",
"quote_error.edit": "Нельга дадаваць цытаты пры рэдагаванні допісаў.",
"quote_error.poll": "Нельга цытаваць з апытаннямі.",
"quote_error.private_mentions": "Цытаванне не дазваляецца ў прамых узгадваннях.",
"quote_error.quote": "За раз дазволена рабіць толькі адну цытату.",
"quote_error.unauthorized": "Вы не ўвайшлі, каб цытаваць гэты допіс.",
"quote_error.upload": "Нельга цытаваць з медыя далучэннямі.",
@@ -911,9 +918,12 @@
"status.pin": "Замацаваць у профілі",
"status.quote": "Цытаваць",
"status.quote.cancel": "Адмяніць цытаванне",
"status.quote_error.blocked_account_hint.title": "Гэты допіс схаваны, бо Вы заблакіравалі @{name}.",
"status.quote_error.blocked_domain_hint.title": "Гэты допіс схаваны, бо Вы заблакіравалі @{domain}.",
"status.quote_error.filtered": "Схавана адным з Вашых фільтраў",
"status.quote_error.limited_account_hint.action": "Усё адно паказаць",
"status.quote_error.limited_account_hint.title": "Гэты ўліковы запіс быў схаваны мадэратарамі {domain}.",
"status.quote_error.muted_account_hint.title": "Гэты допіс схаваны, бо Вы вырашылі ігнараваць @{name}.",
"status.quote_error.not_available": "Допіс недаступны",
"status.quote_error.pending_approval": "Допіс чакае пацвярджэння",
"status.quote_error.pending_approval_popout.body": "У Mastodon можна кантраляваць магчымасць іншых цытаваць Вас. Гэты допіс будзе знаходзіцца ў стане чакання, пакуль мы не атрымаем ухваленне на цытаванне ад аўтара арыгінальнага допісу.",
@@ -1008,6 +1018,8 @@
"video.volume_down": "Паменшыць гучнасць",
"video.volume_up": "Павялічыць гучнасць",
"visibility_modal.button_title": "Вызначыць бачнасць",
"visibility_modal.direct_quote_warning.text": "Калі Вы захавайце бягучыя налады, прымацаваная цытата будзе пераробленая ў спасылку.",
"visibility_modal.direct_quote_warning.title": "Цытаты нельга далучаць да прыватных узгадванняў",
"visibility_modal.header": "Бачнасць і ўзаемадзеянне",
"visibility_modal.helper.direct_quoting": "Прыватныя згадванні, створаныя на Mastodon, нельга цытаваць іншым людзям.",
"visibility_modal.helper.privacy_editing": "Бачнасць нельга змяніць у апублікаваным допісе.",

View File

@@ -190,6 +190,7 @@
"community.column_settings.local_only": "Само локално",
"community.column_settings.media_only": "Само мултимедия",
"community.column_settings.remote_only": "Само отдалечено",
"compose.error.blank_post": "Публикацията не може да е празна.",
"compose.language.change": "Смяна на езика",
"compose.language.search": "Търсене на езици...",
"compose.published.body": "Публикувано.",
@@ -242,6 +243,9 @@
"confirmations.missing_alt_text.secondary": "Все пак да се публикува",
"confirmations.missing_alt_text.title": "Добавяте ли алтернативен текст?",
"confirmations.mute.confirm": "Заглушаване",
"confirmations.private_quote_notify.cancel": "Назад към редактирането",
"confirmations.private_quote_notify.confirm": "Издаване на публикация",
"confirmations.private_quote_notify.do_not_show_again": "Без показване пак на това съобщение",
"confirmations.quiet_post_quote_info.dismiss": "Без друго напомняне",
"confirmations.quiet_post_quote_info.got_it": "Схванах",
"confirmations.quiet_post_quote_info.title": "Цитиране на публикации за тиха публика",

View File

@@ -173,6 +173,8 @@
"column.edit_list": "Edita la llista",
"column.favourites": "Favorits",
"column.firehose": "Tuts en directe",
"column.firehose_local": "Canal en directe per a aquest servidor",
"column.firehose_singular": "Canal en directe",
"column.follow_requests": "Peticions de seguir-te",
"column.home": "Inici",
"column.list_members": "Gestiona els membres de la llista",
@@ -192,6 +194,7 @@
"community.column_settings.local_only": "Només local",
"community.column_settings.media_only": "Només contingut",
"community.column_settings.remote_only": "Només remot",
"compose.error.blank_post": "La publicació no pot estar en blanc.",
"compose.language.change": "Canvia d'idioma",
"compose.language.search": "Cerca idiomes...",
"compose.published.body": "Tut publicat.",
@@ -244,8 +247,13 @@
"confirmations.missing_alt_text.secondary": "Publica-la igualment",
"confirmations.missing_alt_text.title": "Hi voleu afegir text alternatiu?",
"confirmations.mute.confirm": "Silencia",
"confirmations.private_quote_notify.cancel": "Torna a l'edició",
"confirmations.private_quote_notify.message": "La persona que citeu i altres mencionades rebran una notificació i podran veure la vostra publicació, encara que no us segueixen.",
"confirmations.private_quote_notify.title": "Voleu compartir amb seguidors i usuaris mencionats?",
"confirmations.quiet_post_quote_info.dismiss": "No m'ho tornis a recordar",
"confirmations.quiet_post_quote_info.got_it": "Entesos",
"confirmations.quiet_post_quote_info.message": "Quan citeu una publicació pública en mode silenciós, la vostra publicació s'amagarà de les línies de temps de tendències.",
"confirmations.quiet_post_quote_info.title": "Citació d'una publicació pública en mode silenciós",
"confirmations.redraft.confirm": "Esborra i reescriu",
"confirmations.redraft.message": "Segur que vols eliminar aquest tut i tornar a escriure'l? Es perdran tots els impulsos i els favorits, i les respostes al tut original quedaran aïllades.",
"confirmations.redraft.title": "Esborrar i reescriure la publicació?",
@@ -331,6 +339,7 @@
"empty_column.bookmarked_statuses": "Encara no has marcat cap tut. Quan en marquis un, apareixerà aquí.",
"empty_column.community": "La línia de temps local és buida. Escriu alguna cosa públicament per posar-ho tot en marxa!",
"empty_column.direct": "Encara no tens mencions privades. Quan n'enviïs o en rebis una, et sortirà aquí.",
"empty_column.disabled_feed": "Aquest canal ha estat desactivat per l'administració del vostre servidor.",
"empty_column.domain_blocks": "Encara no hi ha dominis blocats.",
"empty_column.explore_statuses": "No hi ha res en tendència ara mateix. Revisa-ho més tard!",
"empty_column.favourited_statuses": "Encara no has afavorit cap tut. Quan ho facis, apareixerà aquí.",
@@ -458,6 +467,7 @@
"ignore_notifications_modal.not_following_title": "Voleu ignorar les notificacions de qui no seguiu?",
"ignore_notifications_modal.private_mentions_title": "Voleu ignorar les notificacions de mencions privades no sol·licitades?",
"info_button.label": "Ajuda",
"info_button.what_is_alt_text": "<h1>Què és el text alternatiu?</h1> <p>El text alternatiu proporciona descripcions d'imatges per a persones amb discapacitat visual, connexions de poca amplada de banda o aquelles que busquen un context addicional.</p> <p>Podeu millorar l'accessibilitat i la comprensió per a tothom escrivint un text alternatiu clar, concís i objectiu.</p> <ul> <li>Descriviu els elements importants</li> <li>Utilitzeu frases senzilles</li> <li>Resumiu el text en imatges</li> <li>Eviteu la informació redundant</li> <li>Centreu-vos en les tendències i els aspectes clau dels elements visuals complexos (com ara diagrames o mapes)</li> </ul>",
"interaction_modal.action": "Per a interactuar amb la publicació de {name} cal que inicieu la sessió en el servidor que feu servir.",
"interaction_modal.go": "Endavant",
"interaction_modal.no_account_yet": "Encara no teniu cap compte?",
@@ -749,7 +759,9 @@
"privacy.unlisted.short": "Públic silenciós",
"privacy_policy.last_updated": "Darrera actualització {date}",
"privacy_policy.title": "Política de Privacitat",
"quote_error.edit": "No es poden afegir cites en editar una publicació.",
"quote_error.poll": "Amb les enquestes no es permeten cites.",
"quote_error.private_mentions": "Amb mencions directes no es permeten cites.",
"quote_error.quote": "Només es permet una cita alhora.",
"quote_error.unauthorized": "No se us permet de citar aquesta publicació.",
"quote_error.upload": "Amb media adjunts no es permeten cites.",
@@ -871,6 +883,7 @@
"status.contains_quote": "Conté una cita",
"status.context.loading": "Es carreguen més respostes",
"status.context.loading_error": "No s'han pogut carregar respostes noves",
"status.context.loading_success": "S'han carregat les noves respostes",
"status.context.more_replies_found": "S'han trobat més respostes",
"status.context.retry": "Torna-ho a provar",
"status.context.show": "Mostra",
@@ -902,9 +915,12 @@
"status.pin": "Fixa en el perfil",
"status.quote": "Cita",
"status.quote.cancel": "Canceŀlar la citació",
"status.quote_error.blocked_account_hint.title": "Aquesta publicació està amagada perquè heu blocat a @{name}.",
"status.quote_error.blocked_domain_hint.title": "Aquesta publicació està amagada perquè heu blocat a {domain}.",
"status.quote_error.filtered": "No es mostra a causa d'un dels vostres filtres",
"status.quote_error.limited_account_hint.action": "Mostra-la igualment",
"status.quote_error.limited_account_hint.title": "Aquest perfil l'han amagat els moderadors de {domain}.",
"status.quote_error.muted_account_hint.title": "Aquesta publicació està amagada perquè heu silenciat a @{name}.",
"status.quote_error.not_available": "Publicació no disponible",
"status.quote_error.pending_approval": "Publicació pendent",
"status.quote_error.pending_approval_popout.body": "A Mastodon pots controlar si algú et pot citar. Aquesta publicació està pendent mentre esperem l'aprovació de l'autor original.",
@@ -999,10 +1015,15 @@
"video.volume_down": "Abaixa el volum",
"video.volume_up": "Apuja el volum",
"visibility_modal.button_title": "Establiu la visibilitat",
"visibility_modal.direct_quote_warning.text": "Si deseu la configuració actual, la cita incrustada es convertirà en un enllaç.",
"visibility_modal.direct_quote_warning.title": "Les cites no es poden incrustar a les mencions privades",
"visibility_modal.header": "Visibilitat i interacció",
"visibility_modal.helper.direct_quoting": "No es poden citar mencions privades fetes a Mastondon.",
"visibility_modal.helper.privacy_editing": "La visibilitat no es pot canviar després de publicar una publicació.",
"visibility_modal.helper.privacy_private_self_quote": "Les autocites de publicacions privades no es poden fer públiques.",
"visibility_modal.helper.private_quoting": "No es poden citar publicacions fetes a Mastodon només per a seguidors.",
"visibility_modal.helper.unlisted_quoting": "Quan la gent et citi les seves publicacions estaran amagades de les línies de temps de tendències.",
"visibility_modal.helper.unlisted_quoting": "Quan la gent us citi, les seves publicacions quedaran amagades de les línies de temps de tendències.",
"visibility_modal.instructions": "Controleu qui pot interactuar amb aquesta publicació. També podeu aplicar la configuració a totes les publicacions futures navegant a <link>Preferències > Valors per defecte de publicació</link>.",
"visibility_modal.privacy_label": "Visibilitat",
"visibility_modal.quote_followers": "Només seguidors",
"visibility_modal.quote_label": "Qui pot citar",

View File

@@ -173,6 +173,8 @@
"column.edit_list": "Upravit seznam",
"column.favourites": "Oblíbené",
"column.firehose": "Živé kanály",
"column.firehose_local": "Živý kanál pro tento server",
"column.firehose_singular": "Živý kanál",
"column.follow_requests": "Žádosti o sledování",
"column.home": "Domů",
"column.list_members": "Spravovat členy seznamu",
@@ -192,6 +194,7 @@
"community.column_settings.local_only": "Pouze místní",
"community.column_settings.media_only": "Pouze média",
"community.column_settings.remote_only": "Pouze vzdálené",
"compose.error.blank_post": "Příspěvek nemůže být prázdný.",
"compose.language.change": "Změnit jazyk",
"compose.language.search": "Prohledat jazyky...",
"compose.published.body": "Příspěvek zveřejněn.",
@@ -244,6 +247,11 @@
"confirmations.missing_alt_text.secondary": "Přesto odeslat",
"confirmations.missing_alt_text.title": "Přidat popisek?",
"confirmations.mute.confirm": "Skrýt",
"confirmations.private_quote_notify.cancel": "Zpět k úpravám",
"confirmations.private_quote_notify.confirm": "Publikovat příspěvek",
"confirmations.private_quote_notify.do_not_show_again": "Nezobrazujte mi znovu tuto zprávu",
"confirmations.private_quote_notify.message": "Osoba, kterou citujete, a další zmínění budou upozorněni a budou moci si zobrazit váš příspěvek, i pokud vás nesledují.",
"confirmations.private_quote_notify.title": "Sdílet se sledujícími a zmíněnými uživateli?",
"confirmations.quiet_post_quote_info.dismiss": "Znovu nepřípomínat",
"confirmations.quiet_post_quote_info.got_it": "Rozumím",
"confirmations.quiet_post_quote_info.message": "Při citování ztišeného veřejného příspěvku, váš příspěvek bude skrytý z os populárních příspěvků.",
@@ -756,6 +764,7 @@
"privacy_policy.title": "Zásady ochrany osobních údajů",
"quote_error.edit": "Citáty nemohou být přidány při úpravě příspěvku.",
"quote_error.poll": "Citování není u dotazníků povoleno.",
"quote_error.private_mentions": "Citování není povoleno s přímými zmínkami.",
"quote_error.quote": "Je povoleno citovat pouze jednou.",
"quote_error.unauthorized": "Nemáte oprávnění citovat tento příspěvek.",
"quote_error.upload": "Není povoleno citovat s přílohami.",
@@ -909,9 +918,12 @@
"status.pin": "Připnout na profil",
"status.quote": "Citovat",
"status.quote.cancel": "Zrušit citování",
"status.quote_error.blocked_account_hint.title": "Tento příspěvek je skryt, protože jste zablokovali @{name}.",
"status.quote_error.blocked_domain_hint.title": "Tento příspěvek je skryt, protože jste zablokovali {domain}.",
"status.quote_error.filtered": "Skryté kvůli jednomu z vašich filtrů",
"status.quote_error.limited_account_hint.action": "Přesto zobrazit",
"status.quote_error.limited_account_hint.title": "Tento účet byl skryt moderátory {domain}.",
"status.quote_error.muted_account_hint.title": "Tento příspěvek je skryt, protože jste ztišili @{name}.",
"status.quote_error.not_available": "Příspěvek není dostupný",
"status.quote_error.pending_approval": "Příspěvek čeká na schválení",
"status.quote_error.pending_approval_popout.body": "Na Mastodonu můžete kontrolovat, zda vás někdo může citovat. Tento příspěvek čeká, dokud neobdržíme schválení od původního autora.",
@@ -1006,6 +1018,8 @@
"video.volume_down": "Snížit hlasitost",
"video.volume_up": "Zvýšit hlasitost",
"visibility_modal.button_title": "Nastavit viditelnost",
"visibility_modal.direct_quote_warning.text": "Pokud uložíte aktuální nastavení, vložená citace bude převedena na odkaz.",
"visibility_modal.direct_quote_warning.title": "Citace nemohou být vloženy do soukromých zmínek",
"visibility_modal.header": "Viditelnost a interakce",
"visibility_modal.helper.direct_quoting": "Soukromé zmínky, které jsou vytvořeny na Mastodonu, nemohou být citovány ostatními.",
"visibility_modal.helper.privacy_editing": "Viditelnost nelze změnit po publikování příspěvku.",

View File

@@ -173,6 +173,8 @@
"column.edit_list": "Golygu rhestr",
"column.favourites": "Ffefrynnau",
"column.firehose": "Ffrydiau byw",
"column.firehose_local": "Ffrwd fyw ar gyfer y gweinydd hwn",
"column.firehose_singular": "Ffrwd fyw",
"column.follow_requests": "Ceisiadau dilyn",
"column.home": "Cartref",
"column.list_members": "Rheoli aelodau rhestr",
@@ -192,6 +194,7 @@
"community.column_settings.local_only": "Lleol yn unig",
"community.column_settings.media_only": "Cyfryngau yn unig",
"community.column_settings.remote_only": "Pell yn unig",
"compose.error.blank_post": "Gall postiad ddim bod yn wag.",
"compose.language.change": "Newid iaith",
"compose.language.search": "Chwilio ieithoedd...",
"compose.published.body": "Postiad wedi ei gyhoeddi.",
@@ -244,6 +247,11 @@
"confirmations.missing_alt_text.secondary": "Postio beth bynnag",
"confirmations.missing_alt_text.title": "Ychwanegu testun amgen?",
"confirmations.mute.confirm": "Tewi",
"confirmations.private_quote_notify.cancel": "Nôl i olygu",
"confirmations.private_quote_notify.confirm": "Cyhoeddi postiad",
"confirmations.private_quote_notify.do_not_show_again": "Peidio dangos y neges hon i mi eto",
"confirmations.private_quote_notify.message": "Bydd y person rydych chi'n ei ddyfynnu a chrybwylliadau eraill yn cael gwybod a bydd yn gallu gweld eich postiad, hyd yn oed os nad ydyn nhw'n eich dilyn chi.",
"confirmations.private_quote_notify.title": "Rhannu gyda dilynwyr a defnyddwyr sy'n cael eu crybwyll?",
"confirmations.quiet_post_quote_info.dismiss": "Peidio fy atgoff eto",
"confirmations.quiet_post_quote_info.got_it": "Iawn",
"confirmations.quiet_post_quote_info.message": "Wrth ddyfynnu postiad cyhoeddus tawel, bydd eich postiad yn cael ei guddio rhag llinellau amser sy'n trendio.",
@@ -756,6 +764,7 @@
"privacy_policy.title": "Polisi Preifatrwydd",
"quote_error.edit": "Does dim modd ychwanegu dyfyniadau wrth olygu postiad.",
"quote_error.poll": "Dyw dyfynnu ddim yn cael ei ganiatáu gyda pholau.",
"quote_error.private_mentions": "Does dim caniatâd i ddyfynnu gyda chrybwylliadau uniongyrchol.",
"quote_error.quote": "Dim ond un dyfyniad ar y tro sy'n cael ei ganiatáu.",
"quote_error.unauthorized": "Does gennych chi ddim awdurdod i ddyfynnu'r postiad hwn.",
"quote_error.upload": "Dyw dyfynnu ddim yn cael ei ganiatáu gydag atodiadau cyfryngau.",
@@ -909,9 +918,12 @@
"status.pin": "Pinio ar y proffil",
"status.quote": "Dyfynnu",
"status.quote.cancel": "Diddymu'r dyfyniad",
"status.quote_error.blocked_account_hint.title": "Mae'r postiad hwn wedi'i guddio oherwydd eich bod wedi rhwystro @{name}.",
"status.quote_error.blocked_domain_hint.title": "Mae'r postiad hwn wedi'i guddio oherwydd eich bod wedi rhwystro {domain}.",
"status.quote_error.filtered": "Wedi'i guddio oherwydd un o'ch hidlwyr",
"status.quote_error.limited_account_hint.action": "Dangos beth bynnag",
"status.quote_error.limited_account_hint.title": "Mae'r cyfrif hwn wedi'i guddio gan gymedrolwyr {domain}.",
"status.quote_error.muted_account_hint.title": "Mae'r postiad hwn wedi'i guddio oherwydd eich bod wedi mudo @{name}.",
"status.quote_error.not_available": "Postiad ddim ar gael",
"status.quote_error.pending_approval": "Postiad yn yr arfaeth",
"status.quote_error.pending_approval_popout.body": "Ar Mastodon, gallwch reoli os yw rhywun yn gallu eich dyfynnu. Mae'r postiad hwn yn cael ei ddal nôl tra'n bod yn cael cymeradwyaeth yr awdur gwreiddiol.",
@@ -1006,6 +1018,8 @@
"video.volume_down": "Lefel sain i lawr",
"video.volume_up": "Lefel sain i fyny",
"visibility_modal.button_title": "Gosod gwelededd",
"visibility_modal.direct_quote_warning.text": "Os byddwch chi'n cadw'r gosodiadau cyfredol, bydd y dyfyniad sydd wedi'i fewnosod yn cael ei drawsnewid yn ddolen.",
"visibility_modal.direct_quote_warning.title": "Does dim modd mewnblannu dyfyniadau mewn crybwylliadau preifat",
"visibility_modal.header": "Gwelededd a rhyngweithio",
"visibility_modal.helper.direct_quoting": "Does dim modd dyfynnu crybwylliadau preifat ysgrifennwyd ar Mastodon.",
"visibility_modal.helper.privacy_editing": "Does dim modd newid gwelededd ar ôl i bostiad gael ei gyhoeddi.",

View File

@@ -194,6 +194,7 @@
"community.column_settings.local_only": "Kun lokalt",
"community.column_settings.media_only": "Kun medier",
"community.column_settings.remote_only": "Kun udefra",
"compose.error.blank_post": "Indlæg kan ikke være tomt.",
"compose.language.change": "Skift sprog",
"compose.language.search": "Søg efter sprog...",
"compose.published.body": "Indlæg udgivet.",
@@ -204,7 +205,7 @@
"compose_form.hashtag_warning": "Da indlægget ikke er offentligt, vises det ikke under noget hashtag, da kun offentlige indlæg er søgbare via hashtags.",
"compose_form.lock_disclaimer": "Din konto er ikke {locked}. Enhver kan følge dig og se indlæg kun beregnet for følgere.",
"compose_form.lock_disclaimer.lock": "låst",
"compose_form.placeholder": "Hvad har du på hjertet?",
"compose_form.placeholder": "Hvad har du på hjerte?",
"compose_form.poll.duration": "Afstemningens varighed",
"compose_form.poll.multiple": "Multivalg",
"compose_form.poll.option_placeholder": "Valgmulighed {number}",
@@ -246,6 +247,11 @@
"confirmations.missing_alt_text.secondary": "Læg op alligevel",
"confirmations.missing_alt_text.title": "Tilføj alt-tekst?",
"confirmations.mute.confirm": "Skjul",
"confirmations.private_quote_notify.cancel": "Tilbage til redigering",
"confirmations.private_quote_notify.confirm": "Offentliggør indlæg",
"confirmations.private_quote_notify.do_not_show_again": "Vis ikke denne besked igen",
"confirmations.private_quote_notify.message": "Den person, du citerer og andre omtalte vil blive underrettet, og vil være i stand til at se dit indlæg, selv om de ikke følger dig.",
"confirmations.private_quote_notify.title": "Del med følgere og omtalte brugere?",
"confirmations.quiet_post_quote_info.dismiss": "Påmind mig ikke igen",
"confirmations.quiet_post_quote_info.got_it": "Forstået",
"confirmations.quiet_post_quote_info.message": "Når du citerer et stille offentligt indlæg, vil dit indlæg blive skjult fra trendtidslinjer.",
@@ -281,7 +287,7 @@
"directory.recently_active": "Aktive for nyligt",
"disabled_account_banner.account_settings": "Kontoindstillinger",
"disabled_account_banner.text": "Din konto {disabledAccount} er pt. deaktiveret.",
"dismissable_banner.community_timeline": "Disse er de seneste offentlige indlæg fra personer med konti hostet af {domain}.",
"dismissable_banner.community_timeline": "Dette er de seneste offentlige indlæg fra personer med konti hostet af {domain}.",
"dismissable_banner.dismiss": "Afvis",
"dismissable_banner.public_timeline": "Dette er de seneste offentlige indlæg fra personer på fediverset, som folk på {domain} følger.",
"domain_block_modal.block": "Blokér server",
@@ -758,6 +764,7 @@
"privacy_policy.title": "Privatlivspolitik",
"quote_error.edit": "Citater kan ikke tilføjes ved redigering af et indlæg.",
"quote_error.poll": "Citering ikke tilladt i afstemninger.",
"quote_error.private_mentions": "Citering er ikke tilladt med direkte omtaler.",
"quote_error.quote": "Kun ét citat ad gangen er tilladt.",
"quote_error.unauthorized": "Du har ikke tilladelse til at citere dette indlæg.",
"quote_error.upload": "Citering ikke tilladt ved medievedhæftninger.",
@@ -911,9 +918,12 @@
"status.pin": "Fastgør til profil",
"status.quote": "Citér",
"status.quote.cancel": "Annullér citat",
"status.quote_error.blocked_account_hint.title": "Dette indlæg er skjult, fordi du har blokeret @{name}.",
"status.quote_error.blocked_domain_hint.title": "Dette indlæg er skjult, fordi du har blokeret @{domain}.",
"status.quote_error.filtered": "Skjult grundet et af filterne",
"status.quote_error.limited_account_hint.action": "Vis alligevel",
"status.quote_error.limited_account_hint.title": "Denne profil er blevet skjult af {domain}-moderatorerne.",
"status.quote_error.muted_account_hint.title": "Dette indlæg er skjult, fordi du har skjult @{name}.",
"status.quote_error.not_available": "Indlæg utilgængeligt",
"status.quote_error.pending_approval": "Afventende indlæg",
"status.quote_error.pending_approval_popout.body": "På Mastodon kan du kontrollere, om nogen kan citere dig. Dette indlæg afventer, mens vi får den oprindelige forfatters godkendelse.",
@@ -1008,6 +1018,8 @@
"video.volume_down": "Lydstyrke ned",
"video.volume_up": "Lydstyrke op",
"visibility_modal.button_title": "Indstil synlighed",
"visibility_modal.direct_quote_warning.text": "Hvis du gemmer de aktuelle indstillinger, vil det indlejrede citat blive konverteret til et link.",
"visibility_modal.direct_quote_warning.title": "Citater kan ikke indlejres i private omtaler",
"visibility_modal.header": "Synlighed og interaktion",
"visibility_modal.helper.direct_quoting": "Private omtaler forfattet på Mastodon kan ikke citeres af andre.",
"visibility_modal.helper.privacy_editing": "Synlighed kan ikke ændres, efter at et indlæg er offentliggjort.",

View File

@@ -43,7 +43,7 @@
"account.follow_back": "Ebenfalls folgen",
"account.follow_back_short": "Ebenfalls folgen",
"account.follow_request": "Anfrage zum Folgen",
"account.follow_request_cancel": "Anfrage zurückziehen",
"account.follow_request_cancel": "Anfrage abbrechen",
"account.follow_request_cancel_short": "Abbrechen",
"account.follow_request_short": "Anfragen",
"account.followers": "Follower",
@@ -173,6 +173,8 @@
"column.edit_list": "Liste bearbeiten",
"column.favourites": "Favoriten",
"column.firehose": "Live-Feeds",
"column.firehose_local": "Live-Feed für diesen Server",
"column.firehose_singular": "Live-Feed",
"column.follow_requests": "Follower-Anfragen",
"column.home": "Startseite",
"column.list_members": "Listenmitglieder verwalten",
@@ -192,6 +194,7 @@
"community.column_settings.local_only": "Nur lokal",
"community.column_settings.media_only": "Nur Beiträge mit Medien",
"community.column_settings.remote_only": "Nur andere Mastodon-Server",
"compose.error.blank_post": "Beitrag muss einen Inhalt haben.",
"compose.language.change": "Sprache festlegen",
"compose.language.search": "Sprachen suchen …",
"compose.published.body": "Beitrag veröffentlicht.",
@@ -244,9 +247,14 @@
"confirmations.missing_alt_text.secondary": "Trotzdem veröffentlichen",
"confirmations.missing_alt_text.title": "Bildbeschreibung hinzufügen?",
"confirmations.mute.confirm": "Stummschalten",
"confirmations.private_quote_notify.cancel": "Zurück zum Bearbeiten",
"confirmations.private_quote_notify.confirm": "Beitrag veröffentlichen",
"confirmations.private_quote_notify.do_not_show_again": "Diesen Hinweis nicht mehr anzeigen",
"confirmations.private_quote_notify.message": "Dein Beitrag wird von dem zitierten sowie den erwähnten Profilen gesehen werden können, auch wenn sie dir nicht folgen.",
"confirmations.private_quote_notify.title": "Mit Followern und erwähnten Profilen teilen?",
"confirmations.quiet_post_quote_info.dismiss": "Nicht mehr anzeigen",
"confirmations.quiet_post_quote_info.got_it": "Verstanden",
"confirmations.quiet_post_quote_info.message": "Beim Zitieren eines Beitrags mit der Sichtbarkeit „Öffentlich (still)“ wird dein zitierter Beitrag ebenfalls nicht in den Trends und öffentlichen Timelines angezeigt.",
"confirmations.quiet_post_quote_info.message": "Beim Zitieren eines Beitrags, dessen Sichtbarkeit „Öffentlich (still)“ ist, wird auch dein Beitrag, der das Zitat enthält, aus den Trends und öffentlichen Timelines ausgeblendet.",
"confirmations.quiet_post_quote_info.title": "Zitieren eines Beitrags mit der Sichtbarkeit „Öffentlich (still)“",
"confirmations.redraft.confirm": "Löschen und neu erstellen",
"confirmations.redraft.message": "Möchtest du diesen Beitrag wirklich löschen und neu verfassen? Alle Favoriten sowie die bisher geteilten Beiträge werden verloren gehen und Antworten auf den ursprünglichen Beitrag verlieren den Zusammenhang.",
@@ -254,15 +262,15 @@
"confirmations.remove_from_followers.confirm": "Follower entfernen",
"confirmations.remove_from_followers.message": "{name} wird dir nicht länger folgen. Bist du dir sicher?",
"confirmations.remove_from_followers.title": "Follower entfernen?",
"confirmations.revoke_quote.confirm": "Beitrag entfernen",
"confirmations.revoke_quote.confirm": "Zitat entfernen",
"confirmations.revoke_quote.message": "Diese Aktion kann nicht rückgängig gemacht werden.",
"confirmations.revoke_quote.title": "Beitrag entfernen?",
"confirmations.unblock.confirm": "Entsperren",
"confirmations.unblock.title": "{name} entsperren?",
"confirmations.revoke_quote.title": "Zitieren meines Beitrags entfernen?",
"confirmations.unblock.confirm": "Nicht mehr blockieren",
"confirmations.unblock.title": "{name} nicht mehr blockieren?",
"confirmations.unfollow.confirm": "Entfolgen",
"confirmations.unfollow.title": "{name} entfolgen?",
"confirmations.withdraw_request.confirm": "Anfrage zurückziehen",
"confirmations.withdraw_request.title": "Anfrage zum Folgen von {name} zurückziehen?",
"confirmations.withdraw_request.title": "Anfrage zum Folgen von {name} widerrufen?",
"content_warning.hide": "Beitrag ausblenden",
"content_warning.show": "Trotzdem anzeigen",
"content_warning.show_more": "Beitrag anzeigen",
@@ -740,22 +748,23 @@
"poll_button.add_poll": "Umfrage erstellen",
"poll_button.remove_poll": "Umfrage entfernen",
"privacy.change": "Sichtbarkeit anpassen",
"privacy.direct.long": "Alle in diesem Beitrag erwähnten Profile",
"privacy.direct.long": "Nur in diesem Beitrag erwähnte Profile",
"privacy.direct.short": "Private Erwähnung",
"privacy.private.long": "Nur deine Follower",
"privacy.private.long": "Nur deine eigenen Follower",
"privacy.private.short": "Follower",
"privacy.public.long": "Alle in und außerhalb von Mastodon",
"privacy.public.long": "Alle innerhalb und außerhalb von Mastodon",
"privacy.public.short": "Öffentlich",
"privacy.quote.anyone": "{visibility} alle dürfen zitieren",
"privacy.quote.disabled": "{visibility} niemand darf zitieren",
"privacy.quote.limited": "{visibility} eingeschränktes Zitieren",
"privacy.unlisted.additional": "Das Verhalten ist wie bei „Öffentlich“, jedoch gibt es einige Einschränkungen. Der Beitrag wird nicht in „Live-Feeds“, „Erkunden“, Hashtags oder über die Mastodon-Suchfunktion auffindbar sein selbst wenn die zugehörige Einstellung aktiviert wurde.",
"privacy.unlisted.long": "Wird nicht in den Suchergebnissen, Trends oder öffentlichen Timelines von Mastodon angezeigt",
"privacy.unlisted.long": "Verborgen vor Suchergebnissen, Trends und öffentlichen Timelines auf Mastodon",
"privacy.unlisted.short": "Öffentlich (still)",
"privacy_policy.last_updated": "Stand: {date}",
"privacy_policy.title": "Datenschutzerklärung",
"quote_error.edit": "Beim Bearbeiten eines Beitrags können keine Zitate hinzugefügt werden.",
"quote_error.poll": "Zitieren ist bei Umfragen nicht gestattet.",
"quote_error.private_mentions": "Das Zitieren ist bei privaten Erwähnungen nicht erlaubt.",
"quote_error.quote": "Es ist jeweils nur ein Zitat zulässig.",
"quote_error.unauthorized": "Du bist nicht berechtigt, diesen Beitrag zu zitieren.",
"quote_error.upload": "Zitieren ist mit Medien-Anhängen nicht möglich.",
@@ -872,7 +881,7 @@
"status.block": "@{name} blockieren",
"status.bookmark": "Lesezeichen setzen",
"status.cancel_reblog_private": "Beitrag nicht mehr teilen",
"status.cannot_quote": "Dir ist es nicht gestattet, diesen Beitrag zu zitieren",
"status.cannot_quote": "Beitrag kann nicht zitiert werden",
"status.cannot_reblog": "Dieser Beitrag kann nicht geteilt werden",
"status.contains_quote": "Enthält Zitat",
"status.context.loading": "Weitere Antworten laden",
@@ -893,7 +902,7 @@
"status.edited_x_times": "{count, plural, one {{count}-mal} other {{count}-mal}} bearbeitet",
"status.embed": "Code zum Einbetten",
"status.favourite": "Favorisieren",
"status.favourites": "{count, plural, one {Mal favorisiert} other {Mal favorisiert}}",
"status.favourites": "{count, plural, one {× favorisiert} other {× favorisiert}}",
"status.filter": "Beitrag filtern",
"status.history.created": "{name} erstellte {date}",
"status.history.edited": "{name} bearbeitete {date}",
@@ -908,10 +917,13 @@
"status.open": "Beitrag öffnen",
"status.pin": "Im Profil anheften",
"status.quote": "Zitieren",
"status.quote.cancel": "Zitat abbrechen",
"status.quote.cancel": "Zitat entfernen",
"status.quote_error.blocked_account_hint.title": "Dieser Beitrag wurde ausgeblendet, weil du @{name} blockiert hast.",
"status.quote_error.blocked_domain_hint.title": "Dieser Beitrag wurde ausgeblendet, weil du {domain} blockiert hast.",
"status.quote_error.filtered": "Ausgeblendet wegen eines deiner Filter",
"status.quote_error.limited_account_hint.action": "Trotzdem anzeigen",
"status.quote_error.limited_account_hint.title": "Dieses Profil wurde von den Moderator*innen von {domain} ausgeblendet.",
"status.quote_error.muted_account_hint.title": "Dieser Beitrag wurde ausgeblendet, weil du @{name} stummgeschaltet hast.",
"status.quote_error.not_available": "Beitrag nicht verfügbar",
"status.quote_error.pending_approval": "Beitragsveröffentlichung ausstehend",
"status.quote_error.pending_approval_popout.body": "Auf Mastodon kann festgelegt werden, ob man zitiert werden möchte. Wir warten auf die Genehmigung des ursprünglichen Profils. Bis dahin steht deine Beitragsveröffentlichung noch aus.",
@@ -920,9 +932,9 @@
"status.quote_manual_review": "Zitierte*r überprüft manuell",
"status.quote_noun": "Zitat",
"status.quote_policy_change": "Ändern, wer zitieren darf",
"status.quote_post_author": "Zitierte einen Beitrag von @{name}",
"status.quote_post_author": "Zitierter Beitrag von @{name}",
"status.quote_private": "Private Beiträge können nicht zitiert werden",
"status.quotes": "{count, plural, one {Mal zitiert} other {Mal zitiert}}",
"status.quotes": "{count, plural, one {× zitiert} other {× zitiert}}",
"status.quotes.empty": "Diesen Beitrag hat bisher noch niemand zitiert. Sobald es jemand tut, wird das Profil hier erscheinen.",
"status.quotes.local_other_disclaimer": "Durch Autor*in abgelehnte Zitate werden nicht angezeigt.",
"status.quotes.remote_other_disclaimer": "Nur Zitate von {domain} werden hier garantiert angezeigt. Durch Autor*in abgelehnte Zitate werden nicht angezeigt.",
@@ -931,7 +943,7 @@
"status.reblog_or_quote": "Teilen oder zitieren",
"status.reblog_private": "Erneut mit deinen Followern teilen",
"status.reblogged_by": "{name} teilte",
"status.reblogs": "{count, plural, one {Mal geteilt} other {Mal geteilt}}",
"status.reblogs": "{count, plural, one {× geteilt} other {× geteilt}}",
"status.reblogs.empty": "Diesen Beitrag hat bisher noch niemand geteilt. Sobald es jemand tut, wird das Profil hier erscheinen.",
"status.redraft": "Löschen und neu erstellen",
"status.remove_bookmark": "Lesezeichen entfernen",
@@ -943,7 +955,7 @@
"status.replyAll": "Allen antworten",
"status.report": "@{name} melden",
"status.request_quote": "Anfrage zum Zitieren",
"status.revoke_quote": "Meinen zitierten Beitrag aus dem Beitrag von @{name} entfernen",
"status.revoke_quote": "Zitat bei @{name} entfernen",
"status.sensitive_warning": "Inhaltswarnung",
"status.share": "Teilen",
"status.show_less_all": "Alles einklappen",
@@ -1006,7 +1018,9 @@
"video.volume_down": "Leiser",
"video.volume_up": "Lauter",
"visibility_modal.button_title": "Sichtbarkeit festlegen",
"visibility_modal.header": "Sichtbarkeit und Interaktion",
"visibility_modal.direct_quote_warning.text": "Wenn diese Einstellungen gespeichert werden, wird das eingebettete Zitat in einen Link umgewandelt.",
"visibility_modal.direct_quote_warning.title": "Zitate können in privaten Erwähnungen nicht eingebettet werden",
"visibility_modal.header": "Sichtbarkeit und Zitate",
"visibility_modal.helper.direct_quoting": "Private Erwähnungen, die auf Mastodon verfasst wurden, können nicht von anderen zitiert werden.",
"visibility_modal.helper.privacy_editing": "Die Sichtbarkeit eines bereits veröffentlichten Beitrags kann nachträglich nicht mehr geändert werden.",
"visibility_modal.helper.privacy_private_self_quote": "Beiträge mit privaten Erwähnungen können öffentlich nicht zitiert werden.",
@@ -1015,7 +1029,7 @@
"visibility_modal.instructions": "Lege fest, wer mit diesem Beitrag interagieren darf. Du hast auch die Möglichkeit, diese Einstellung auf alle zukünftigen Beiträge anzuwenden. Gehe zu: <link>Einstellungen > Standardeinstellungen für Beiträge</link>",
"visibility_modal.privacy_label": "Sichtbarkeit",
"visibility_modal.quote_followers": "Nur Follower",
"visibility_modal.quote_label": "Wer zitieren darf",
"visibility_modal.quote_label": "Wer darf mich zitieren?",
"visibility_modal.quote_nobody": "Nur ich",
"visibility_modal.quote_public": "Alle",
"visibility_modal.save": "Speichern"

View File

@@ -194,6 +194,7 @@
"community.column_settings.local_only": "Τοπικά μόνο",
"community.column_settings.media_only": "Μόνο πολυμέσα",
"community.column_settings.remote_only": "Απομακρυσμένα μόνο",
"compose.error.blank_post": "Η ανάρτηση δεν μπορεί να είναι κενή.",
"compose.language.change": "Αλλαγή γλώσσας",
"compose.language.search": "Αναζήτηση γλωσσών...",
"compose.published.body": "Η ανάρτηση δημοσιεύτηκε.",
@@ -246,6 +247,11 @@
"confirmations.missing_alt_text.secondary": "Δημοσίευση όπως και να ΄χει",
"confirmations.missing_alt_text.title": "Προσθήκη εναλλακτικού κειμένου;",
"confirmations.mute.confirm": "Αποσιώπηση",
"confirmations.private_quote_notify.cancel": "Πίσω στην επεξεργασία",
"confirmations.private_quote_notify.confirm": "Δημοσίευση ανάρτησης",
"confirmations.private_quote_notify.do_not_show_again": "Να μην εμφανιστεί ξανά αυτό το μήνυμα",
"confirmations.private_quote_notify.message": "Το άτομο που παραθέτετε και άλλες επισημάνσεις θα ειδοποιηθούν και θα μπορούν να δουν την ανάρτησή σας, ακόμη και αν δεν σας ακολουθούν.",
"confirmations.private_quote_notify.title": "Κοινοποίηση με τους ακολούθους και τους επισημασμένους χρήστες;",
"confirmations.quiet_post_quote_info.dismiss": "Μη μου το ξαναθυμίσεις",
"confirmations.quiet_post_quote_info.got_it": "Το κατάλαβα",
"confirmations.quiet_post_quote_info.message": "Όταν παραθέτετε μια ήσυχη δημόσια ανάρτηση, η ανάρτηση σας θα είναι κρυμμένη από τις δημοφιλείς ροές.",
@@ -758,6 +764,7 @@
"privacy_policy.title": "Πολιτική Απορρήτου",
"quote_error.edit": "Δεν μπορούν να προστεθούν παραθέσεις κατά την επεξεργασία μιας ανάρτησης.",
"quote_error.poll": "Η παράθεση δεν επιτρέπεται με δημοσκοπήσεις.",
"quote_error.private_mentions": "Η παράθεση δεν επιτρέπεται με άμεσες επισημάνσεις.",
"quote_error.quote": "Επιτρέπεται μόνο μία παράθεση τη φορά.",
"quote_error.unauthorized": "Δεν είστε εξουσιοδοτημένοι να παραθέσετε αυτή την ανάρτηση.",
"quote_error.upload": "Η παράθεση δεν επιτρέπεται με συνημμένα πολυμέσων.",
@@ -911,9 +918,12 @@
"status.pin": "Καρφίτσωσε στο προφίλ",
"status.quote": "Παράθεση",
"status.quote.cancel": "Ακύρωση παράθεσης",
"status.quote_error.blocked_account_hint.title": "Αυτή η ανάρτηση είναι κρυμμένη επειδή έχετε μπλοκάρει τον/την @{name}.",
"status.quote_error.blocked_domain_hint.title": "Αυτή η ανάρτηση είναι κρυμμένη επειδή έχετε μπλοκάρει το {domain}.",
"status.quote_error.filtered": "Κρυφό λόγω ενός από τα φίλτρα σου",
"status.quote_error.limited_account_hint.action": "Εμφάνιση ούτως ή άλλως",
"status.quote_error.limited_account_hint.title": "Αυτό το προφίλ έχει αποκρυφτεί από τους διαχειριστές του διακομιστή {domain}.",
"status.quote_error.muted_account_hint.title": "Αυτή η ανάρτηση είναι κρυμμένη επειδή έχετε κάνει σίγαση τον/την @{name}.",
"status.quote_error.not_available": "Ανάρτηση μη διαθέσιμη",
"status.quote_error.pending_approval": "Ανάρτηση σε αναμονή",
"status.quote_error.pending_approval_popout.body": "Στο Mastodon, μπορείς να ελέγξεις αν κάποιος μπορεί να σε παραθέσει. Αυτή η ανάρτηση εκκρεμεί ενώ λαμβάνουμε την έγκριση του αρχικού συντάκτη.",
@@ -1008,6 +1018,8 @@
"video.volume_down": "Μείωση έντασης",
"video.volume_up": "Αύξηση έντασης",
"visibility_modal.button_title": "Ορισμός ορατότητας",
"visibility_modal.direct_quote_warning.text": "Εάν αποθηκεύσετε τις τρέχουσες ρυθμίσεις, η ενσωματωμένη παράθεση θα μετατραπεί σε σύνδεσμο.",
"visibility_modal.direct_quote_warning.title": "Οι παραθέσεις δεν μπορούν να ενσωματωθούν σε ιδιωτικές επισημάνσεις",
"visibility_modal.header": "Ορατότητα και αλληλεπίδραση",
"visibility_modal.helper.direct_quoting": "Ιδιωτικές αναφορές που έχουν συνταχθεί στο Mastodon δεν μπορούν να γίνουν παράθεση από άλλους.",
"visibility_modal.helper.privacy_editing": "Η ορατότητα δεν μπορεί να αλλάξει μετά τη δημοσίευση μιας ανάρτησης.",

View File

@@ -28,6 +28,7 @@
"account.disable_notifications": "Stop notifying me when @{name} posts",
"account.domain_blocking": "Blocking domain",
"account.edit_profile": "Edit profile",
"account.edit_profile_short": "Edit",
"account.enable_notifications": "Notify me when @{name} posts",
"account.endorse": "Feature on profile",
"account.familiar_followers_many": "Followed by {name1}, {name2}, and {othersCount, plural, one {one other you know} other {# others you know}}",
@@ -40,6 +41,11 @@
"account.featured_tags.last_status_never": "No posts",
"account.follow": "Follow",
"account.follow_back": "Follow back",
"account.follow_back_short": "Follow back",
"account.follow_request": "Request to follow",
"account.follow_request_cancel": "Cancel request",
"account.follow_request_cancel_short": "Cancel",
"account.follow_request_short": "Request",
"account.followers": "Followers",
"account.followers.empty": "No one follows this user yet.",
"account.followers_counter": "{count, plural, one {{counter} follower} other {{counter} followers}}",
@@ -167,6 +173,8 @@
"column.edit_list": "Edit list",
"column.favourites": "Favourites",
"column.firehose": "Live feeds",
"column.firehose_local": "Live feed for this server",
"column.firehose_singular": "Live feed",
"column.follow_requests": "Follow requests",
"column.home": "Home",
"column.list_members": "Manage list members",
@@ -186,6 +194,7 @@
"community.column_settings.local_only": "Local only",
"community.column_settings.media_only": "Media Only",
"community.column_settings.remote_only": "Remote only",
"compose.error.blank_post": "Post can't be blank.",
"compose.language.change": "Change language",
"compose.language.search": "Search languages...",
"compose.published.body": "Post published.",
@@ -238,13 +247,30 @@
"confirmations.missing_alt_text.secondary": "Post anyway",
"confirmations.missing_alt_text.title": "Add alt text?",
"confirmations.mute.confirm": "Mute",
"confirmations.private_quote_notify.cancel": "Back to editing",
"confirmations.private_quote_notify.confirm": "Publish post",
"confirmations.private_quote_notify.do_not_show_again": "Don't show me this message again",
"confirmations.private_quote_notify.message": "The person you are quoting and other mentions will be notified and will be able to view your post, even if they're not following you.",
"confirmations.private_quote_notify.title": "Share with followers and mentioned users?",
"confirmations.quiet_post_quote_info.dismiss": "Don't remind me again",
"confirmations.quiet_post_quote_info.got_it": "Got it",
"confirmations.quiet_post_quote_info.message": "When quoting a quiet public post, your post will be hidden from trending timelines.",
"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.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?",
"confirmations.revoke_quote.confirm": "Remove post",
"confirmations.revoke_quote.message": "This action cannot be undone.",
"confirmations.revoke_quote.title": "Remove post?",
"confirmations.unblock.confirm": "Unblock",
"confirmations.unblock.title": "Unblock {name}?",
"confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.title": "Unfollow {name}?",
"confirmations.withdraw_request.confirm": "Withdraw request",
"confirmations.withdraw_request.title": "Withdraw request to follow {name}?",
"content_warning.hide": "Hide post",
"content_warning.show": "Show anyway",
"content_warning.show_more": "Show more",
@@ -286,6 +312,7 @@
"domain_pill.your_handle": "Your handle:",
"domain_pill.your_server": "Your digital home, where all of your posts live. Dont like this one? Transfer servers at any time and bring your followers, too.",
"domain_pill.your_username": "Your unique identifier on this server. Its 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.",
"embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity",
@@ -314,6 +341,7 @@
"empty_column.bookmarked_statuses": "You don't have any bookmarked posts yet. When you bookmark one, it will show up here.",
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
"empty_column.direct": "You don't have any private mentions yet. When you send or receive one, it will show up here.",
"empty_column.disabled_feed": "This feed has been disabled by your server administrators.",
"empty_column.domain_blocks": "There are no blocked domains yet.",
"empty_column.explore_statuses": "Nothing is trending right now. Check back later!",
"empty_column.favourited_statuses": "You don't have any favourite posts yet. When you favourite one, it will show up here.",
@@ -442,10 +470,12 @@
"ignore_notifications_modal.private_mentions_title": "Ignore notifications from unsolicited Private Mentions?",
"info_button.label": "Help",
"info_button.what_is_alt_text": "<h1>What is alt text?</h1> <p>Alt text provides image descriptions for people with vision impairments, low-bandwidth connections, or those seeking extra context.</p> <p>You can improve accessibility and understanding for everyone by writing clear, concise, and objective alt text.</p> <ul> <li>Capture important elements</li> <li>Summarise text in images</li> <li>Use regular sentence structure</li> <li>Avoid redundant information</li> <li>Focus on trends and key findings in complex visuals (like diagrams or maps)</li> </ul>",
"interaction_modal.action": "To interact with {name}'s post, you need to sign into your account on whatever Mastodon server you use.",
"interaction_modal.go": "Go",
"interaction_modal.no_account_yet": "Don't have an account yet?",
"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}",
"intervals.full.days": "{number, plural, one {# day} other {# days}}",
"intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
@@ -466,6 +496,7 @@
"keyboard_shortcuts.home": "Open home timeline",
"keyboard_shortcuts.hotkey": "Hotkey",
"keyboard_shortcuts.legend": "to display this legend",
"keyboard_shortcuts.load_more": "Focus \"Load more\" button",
"keyboard_shortcuts.local": "to open local timeline",
"keyboard_shortcuts.mention": "to mention author",
"keyboard_shortcuts.muted": "to open muted users list",
@@ -474,6 +505,7 @@
"keyboard_shortcuts.open_media": "to open media",
"keyboard_shortcuts.pinned": "to open pinned posts list",
"keyboard_shortcuts.profile": "to open author's profile",
"keyboard_shortcuts.quote": "Quote post",
"keyboard_shortcuts.reply": "to reply",
"keyboard_shortcuts.requests": "to open follow requests list",
"keyboard_shortcuts.search": "to focus search",
@@ -485,6 +517,8 @@
"keyboard_shortcuts.translate": "to translate a post",
"keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
"keyboard_shortcuts.up": "Move up in the list",
"learn_more_link.got_it": "Got it",
"learn_more_link.learn_more": "Learn more",
"lightbox.close": "Close",
"lightbox.next": "Next",
"lightbox.previous": "Previous",
@@ -585,6 +619,7 @@
"notification.label.mention": "Mention",
"notification.label.private_mention": "Private mention",
"notification.label.private_reply": "Private reply",
"notification.label.quote": "{name} quoted your post",
"notification.label.reply": "Reply",
"notification.mention": "Mention",
"notification.mentioned_you": "{name} mentioned you",
@@ -599,6 +634,7 @@
"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.quoted_update": "{name} edited a post you have quoted",
"notification.reblog": "{name} boosted your post",
"notification.reblog.name_and_others_with_link": "{name} and <a>{count, plural, one {# other} other {# others}}</a> boosted your post",
"notification.relationships_severance_event": "Lost connections with {name}",
@@ -642,6 +678,7 @@
"notifications.column_settings.mention": "Mentions:",
"notifications.column_settings.poll": "Poll results:",
"notifications.column_settings.push": "Push notifications",
"notifications.column_settings.quote": "Quotes:",
"notifications.column_settings.reblog": "Boosts:",
"notifications.column_settings.show": "Show in column",
"notifications.column_settings.sound": "Play sound",
@@ -717,10 +754,18 @@
"privacy.private.short": "Followers",
"privacy.public.long": "Anyone on and off Mastodon",
"privacy.public.short": "Public",
"privacy.quote.anyone": "{visibility}, anyone can quote",
"privacy.quote.disabled": "{visibility}, quotes disabled",
"privacy.quote.limited": "{visibility}, quotes limited",
"privacy.unlisted.additional": "This behaves exactly like public, except the post will not appear in live feeds or hashtags, explore, or Mastodon search, even if you are opted-in account-wide.",
"privacy.unlisted.long": "Hidden from Mastodon search results, trending, and public timelines",
"privacy.unlisted.short": "Quiet public",
"privacy_policy.last_updated": "Last updated {date}",
"privacy_policy.title": "Privacy Policy",
"quote_error.edit": "Quotes cannot be added when editing a post.",
"quote_error.poll": "Quoting is not allowed with polls.",
"quote_error.private_mentions": "Quoting is not allowed with direct mentions.",
"quote_error.quote": "Only one quote at a time is allowed.",
"recommended": "Recommended",
"refresh": "Refresh",
"regeneration_indicator.please_stand_by": "Please stand by.",
@@ -929,5 +974,13 @@
"video.skip_forward": "Skip forward",
"video.unmute": "Unmute",
"video.volume_down": "Volume down",
"video.volume_up": "Volume up"
"video.volume_up": "Volume up",
"visibility_modal.helper.unlisted_quoting": "When people quote you, their post will also be hidden from trending timelines.",
"visibility_modal.instructions": "Control who can interact with this post. You can also apply settings to all future posts by navigating to <link>Preferences > Posting defaults</link>.",
"visibility_modal.privacy_label": "Visibility",
"visibility_modal.quote_followers": "Followers only",
"visibility_modal.quote_label": "Who can quote",
"visibility_modal.quote_nobody": "Just me",
"visibility_modal.quote_public": "Anyone",
"visibility_modal.save": "Save"
}

View File

@@ -194,6 +194,7 @@
"community.column_settings.local_only": "Local only",
"community.column_settings.media_only": "Media Only",
"community.column_settings.remote_only": "Remote only",
"compose.error.blank_post": "Post can't be blank.",
"compose.language.change": "Change language",
"compose.language.search": "Search languages...",
"compose.published.body": "Post published.",
@@ -246,6 +247,11 @@
"confirmations.missing_alt_text.secondary": "Post anyway",
"confirmations.missing_alt_text.title": "Add alt text?",
"confirmations.mute.confirm": "Mute",
"confirmations.private_quote_notify.cancel": "Back to editing",
"confirmations.private_quote_notify.confirm": "Publish post",
"confirmations.private_quote_notify.do_not_show_again": "Don't show me this message again",
"confirmations.private_quote_notify.message": "The person you are quoting and other mentions will be notified and will be able to view your post, even if they're not following you.",
"confirmations.private_quote_notify.title": "Share with followers and mentioned users?",
"confirmations.quiet_post_quote_info.dismiss": "Don't remind me again",
"confirmations.quiet_post_quote_info.got_it": "Got it",
"confirmations.quiet_post_quote_info.message": "When quoting a quiet public post, your post will be hidden from trending timelines.",
@@ -758,6 +764,7 @@
"privacy_policy.title": "Privacy Policy",
"quote_error.edit": "Quotes cannot be added when editing a post.",
"quote_error.poll": "Quoting is not allowed with polls.",
"quote_error.private_mentions": "Quoting is not allowed with direct mentions.",
"quote_error.quote": "Only one quote at a time is allowed.",
"quote_error.unauthorized": "You are not authorized to quote this post.",
"quote_error.upload": "Quoting is not allowed with media attachments.",
@@ -1011,6 +1018,8 @@
"video.volume_down": "Volume down",
"video.volume_up": "Volume up",
"visibility_modal.button_title": "Set visibility",
"visibility_modal.direct_quote_warning.text": "If you save the current settings, the embedded quote will be converted to a link.",
"visibility_modal.direct_quote_warning.title": "Quotes can't be embedded in private mentions",
"visibility_modal.header": "Visibility and interaction",
"visibility_modal.helper.direct_quoting": "Private mentions authored on Mastodon can't be quoted by others.",
"visibility_modal.helper.privacy_editing": "Visibility can't be changed after a post is published.",

View File

@@ -39,6 +39,7 @@
"account.featured_tags.last_status_never": "Neniu afiŝo",
"account.follow": "Sekvi",
"account.follow_back": "Sekvu reen",
"account.follow_back_short": "Sekvu reen",
"account.followers": "Sekvantoj",
"account.followers.empty": "Ankoraŭ neniu sekvas ĉi tiun uzanton.",
"account.followers_counter": "{count, plural, one{{counter} sekvanto} other {{counter} sekvantoj}}",

View File

@@ -194,6 +194,7 @@
"community.column_settings.local_only": "Sólo local",
"community.column_settings.media_only": "Sólo medios",
"community.column_settings.remote_only": "Sólo remoto",
"compose.error.blank_post": "El mensaje no puede estar en blanco.",
"compose.language.change": "Cambiar idioma",
"compose.language.search": "Buscar idiomas…",
"compose.published.body": "Mensaje publicado.",
@@ -246,6 +247,11 @@
"confirmations.missing_alt_text.secondary": "Enviar de todos modos",
"confirmations.missing_alt_text.title": "¿Agregar texto alternativo?",
"confirmations.mute.confirm": "Silenciar",
"confirmations.private_quote_notify.cancel": "Volver a editar",
"confirmations.private_quote_notify.confirm": "Enviar mensaje",
"confirmations.private_quote_notify.do_not_show_again": "No mostrarme este mensaje de nuevo",
"confirmations.private_quote_notify.message": "La cuenta a la que estás citando y otras menciones serán notificadas y podrán ver tu mensaje, incluso si no te están siguiendo.",
"confirmations.private_quote_notify.title": "¿Compartir con seguidores y usuarios mencionados?",
"confirmations.quiet_post_quote_info.dismiss": "No recordar de nuevo",
"confirmations.quiet_post_quote_info.got_it": "Entendido",
"confirmations.quiet_post_quote_info.message": "Al citar un mensaje público pero silencioso, tu mensaje se ocultará de las líneas temporales de tendencias.",
@@ -735,10 +741,10 @@
"poll.refresh": "Refrescar",
"poll.reveal": "Ver resultados",
"poll.total_people": "{count, plural, one {# persona} other {# personas}}",
"poll.total_votes": "{count, plural, one {# voto} other {# votos}}",
"poll.total_votes": "{count, plural, one {voto} other {votos}}",
"poll.vote": "Votar",
"poll.voted": "Votaste esta opción",
"poll.votes": "{votes, plural, one {# voto} other {# votos}}",
"poll.votes": "{votes, plural, one {voto} other {votos}}",
"poll_button.add_poll": "Agregar encuesta",
"poll_button.remove_poll": "Quitar encuesta",
"privacy.change": "Configurar privacidad del mensaje",
@@ -753,11 +759,12 @@
"privacy.quote.limited": "{visibility}, citas limitadas",
"privacy.unlisted.additional": "Esto se comporta exactamente igual que con la configuración de privacidad de mensaje «Público», excepto que el mensaje no aparecerá en las líneas temporales en vivo, ni en las etiquetas, ni en la línea temporal «Explorá», ni en la búsqueda de Mastodon; incluso si optaste por hacer tu cuenta visible.",
"privacy.unlisted.long": "Oculto de los resultados de búsqueda, tendencias y líneas temporales públicas de Mastodon",
"privacy.unlisted.short": "Público silencioso",
"privacy.unlisted.short": "Público pero silencioso",
"privacy_policy.last_updated": "Última actualización: {date}",
"privacy_policy.title": "Política de privacidad",
"quote_error.edit": "Las citas no se pueden agregar al editar un mensaje.",
"quote_error.poll": "No se permite citar encuestas.",
"quote_error.private_mentions": "No se permite citar con menciones directas.",
"quote_error.quote": "Solo se permite una cita a la vez.",
"quote_error.unauthorized": "No tenés autorización para citar este mensaje.",
"quote_error.upload": "No se permite citar con archivos multimedia.",
@@ -911,9 +918,12 @@
"status.pin": "Fijar en el perfil",
"status.quote": "Citar",
"status.quote.cancel": "Cancelar cita",
"status.quote_error.blocked_account_hint.title": "Este mensaje está oculto porque bloqueaste a @{name}.",
"status.quote_error.blocked_domain_hint.title": "Este mensaje está oculto porque bloqueaste {domain}.",
"status.quote_error.filtered": "Oculto debido a uno de tus filtros",
"status.quote_error.limited_account_hint.action": "Mostrar de todos modos",
"status.quote_error.limited_account_hint.title": "Esta cuenta fue ocultada por los moderadores de {domain}.",
"status.quote_error.muted_account_hint.title": "Este mensaje está oculto porque silenciaste a @{name}.",
"status.quote_error.not_available": "Mensaje no disponible",
"status.quote_error.pending_approval": "Mensaje pendiente",
"status.quote_error.pending_approval_popout.body": "En Mastodon, podés controlar si alguien te puede citar. Este mensaje está pendiente hasta obtener la aprobación del autor original.",
@@ -924,7 +934,7 @@
"status.quote_policy_change": "Cambiá quién puede citar",
"status.quote_post_author": "Se citó un mensaje de @{name}",
"status.quote_private": "No se pueden citar los mensajes privados",
"status.quotes": "{count, plural, one {# voto} other {# votos}}",
"status.quotes": "{count, plural, one {voto} other {votos}}",
"status.quotes.empty": "Todavía nadie citó este mensaje. Cuando alguien lo haga, se mostrará acá.",
"status.quotes.local_other_disclaimer": "Las citas rechazadas por el autor no serán mostradas.",
"status.quotes.remote_other_disclaimer": "Solo las citas de {domain} están garantizadas de ser mostradas acá. Las citas rechazadas por el autor no serán mostradas.",
@@ -1008,6 +1018,8 @@
"video.volume_down": "Bajar volumen",
"video.volume_up": "Subir volumen",
"visibility_modal.button_title": "Establecer visibilidad",
"visibility_modal.direct_quote_warning.text": "Si guardás la configuración actual, la cita insertada se convertirá en un enlace.",
"visibility_modal.direct_quote_warning.title": "Las citas no pueden ser insertadas en menciones privadas",
"visibility_modal.header": "Visibilidad e interacción",
"visibility_modal.helper.direct_quoting": "Las menciones privadas redactadas en Mastodon no pueden ser citadas por otras cuentas.",
"visibility_modal.helper.privacy_editing": "La visibilidad no se puede cambiar después de que se haya enviado un mensaje.",

View File

@@ -42,7 +42,7 @@
"account.follow": "Seguir",
"account.follow_back": "Seguir también",
"account.follow_back_short": "Seguir también",
"account.follow_request": "Solicitud de seguimiento",
"account.follow_request": "Solicitar seguimiento",
"account.follow_request_cancel": "Cancelar solicitud",
"account.follow_request_cancel_short": "Cancelar",
"account.follow_request_short": "Solicitar",
@@ -172,9 +172,9 @@
"column.domain_blocks": "Dominios ocultados",
"column.edit_list": "Editar lista",
"column.favourites": "Favoritos",
"column.firehose": "Cronologías",
"column.firehose_local": "Cronología para este servidor",
"column.firehose_singular": "Cronología",
"column.firehose": "Feeds en vivo",
"column.firehose_local": "Feed en vivo para este servidor",
"column.firehose_singular": "Feed en vivo",
"column.follow_requests": "Solicitudes de seguimiento",
"column.home": "Inicio",
"column.list_members": "Administrar miembros de la lista",
@@ -194,6 +194,7 @@
"community.column_settings.local_only": "Solo local",
"community.column_settings.media_only": "Solo media",
"community.column_settings.remote_only": "Solo remoto",
"compose.error.blank_post": "La publicación no puede estar vacía.",
"compose.language.change": "Cambiar idioma",
"compose.language.search": "Buscar idiomas...",
"compose.published.body": "Publicado.",
@@ -246,6 +247,11 @@
"confirmations.missing_alt_text.secondary": "Publicar de todas maneras",
"confirmations.missing_alt_text.title": "¿Añadir texto alternativo?",
"confirmations.mute.confirm": "Silenciar",
"confirmations.private_quote_notify.cancel": "Seguir editando",
"confirmations.private_quote_notify.confirm": "Publicar",
"confirmations.private_quote_notify.do_not_show_again": "No mostrar este mensaje de nuevo",
"confirmations.private_quote_notify.message": "Tu publicación será notificada y podrá ser vista por la persona a la que mencionas y otras menciones, aún si no te siguen.",
"confirmations.private_quote_notify.title": "¿Compartir con seguidores y usuarios mencionados?",
"confirmations.quiet_post_quote_info.dismiss": "No me lo recuerdes otra vez",
"confirmations.quiet_post_quote_info.got_it": "Entendido",
"confirmations.quiet_post_quote_info.message": "Al citar una publicación pública discreta, tu publicación se ocultará de las cronologías de tendencias.",
@@ -263,8 +269,8 @@
"confirmations.unblock.title": "¿Desbloquear a {name}?",
"confirmations.unfollow.confirm": "Dejar de seguir",
"confirmations.unfollow.title": "¿Dejar de seguir a {name}?",
"confirmations.withdraw_request.confirm": "Retirar solicitud",
"confirmations.withdraw_request.title": "¿Retirar solicitud de seguimiento a {name}?",
"confirmations.withdraw_request.confirm": "Cancelar solicitud",
"confirmations.withdraw_request.title": "¿Cancelar solicitud para seguir a {name}?",
"content_warning.hide": "Ocultar publicación",
"content_warning.show": "Mostrar de todos modos",
"content_warning.show_more": "Mostrar más",
@@ -335,7 +341,7 @@
"empty_column.bookmarked_statuses": "Aún no tienes ninguna publicación guardada como marcador. Cuando guardes una, se mostrará aquí.",
"empty_column.community": "La cronología local está vacía. ¡Escribe algo públicamente para ponerla en marcha!",
"empty_column.direct": "Aún no tienes menciones privadas. Cuando envíes o recibas una, aparecerán aquí.",
"empty_column.disabled_feed": "Esta cronología ha sido desactivada por los administradores del servidor.",
"empty_column.disabled_feed": "Este feed fue desactivado por los administradores de tu servidor.",
"empty_column.domain_blocks": "Todavía no hay dominios ocultos.",
"empty_column.explore_statuses": "Nada es tendencia en este momento. ¡Revisa más tarde!",
"empty_column.favourited_statuses": "Todavía no tienes publicaciones favoritas. Cuando le des favorito a una publicación se mostrarán acá.",
@@ -756,8 +762,9 @@
"privacy.unlisted.short": "Pública, pero discreta",
"privacy_policy.last_updated": "Actualizado por última vez {date}",
"privacy_policy.title": "Política de Privacidad",
"quote_error.edit": "No se pueden añadir citas cuando se edita una publicación.",
"quote_error.edit": "No se pueden añadir citas mientras un post está siendo editado.",
"quote_error.poll": "No se permite citar encuestas.",
"quote_error.private_mentions": "Citar no está disponible sin menciones directas.",
"quote_error.quote": "Solo se permite una cita a la vez.",
"quote_error.unauthorized": "No estás autorizado a citar esta publicación.",
"quote_error.upload": "No se permite citar con archivos multimedia.",
@@ -797,7 +804,7 @@
"report.forward": "Reenviar a {target}",
"report.forward_hint": "Esta cuenta es de otro servidor. ¿Enviar una copia anonimizada del informe allí también?",
"report.mute": "Silenciar",
"report.mute_explanation": "No veras sus publiaciones. Todavía pueden seguirte y ver tus publicaciones y no sabrán que están silenciados.",
"report.mute_explanation": "No verás sus publicaciones. Todavía pueden seguirte y ver tus publicaciones y no sabrán que están silenciados.",
"report.next": "Siguiente",
"report.placeholder": "Comentarios adicionales",
"report.reasons.dislike": "No me gusta",
@@ -879,7 +886,7 @@
"status.contains_quote": "Contiene cita",
"status.context.loading": "Cargando más respuestas",
"status.context.loading_error": "No se pudieron cargar nuevas respuestas",
"status.context.loading_success": "Cargadas nuevas respuestas",
"status.context.loading_success": "Nuevas respuestas cargadas",
"status.context.more_replies_found": "Se han encontrado más respuestas",
"status.context.retry": "Reintentar",
"status.context.show": "Mostrar",
@@ -911,9 +918,12 @@
"status.pin": "Fijar",
"status.quote": "Citar",
"status.quote.cancel": "Cancelar cita",
"status.quote_error.blocked_account_hint.title": "Esta publicación se ocultó porque bloqueaste a @{name}.",
"status.quote_error.blocked_domain_hint.title": "Este post está oculto porque bloqueaste {domain}.",
"status.quote_error.filtered": "Oculto debido a uno de tus filtros",
"status.quote_error.limited_account_hint.action": "Mostrar de todas formas",
"status.quote_error.limited_account_hint.title": "Esta cuenta ha sido ocultada por los moderadores de {domain}.",
"status.quote_error.muted_account_hint.title": "Esta publicación está oculta porque silenciaste a @{name}.",
"status.quote_error.not_available": "Publicación no disponible",
"status.quote_error.pending_approval": "Publicación pendiente",
"status.quote_error.pending_approval_popout.body": "En Mastodon, puedes controlar si alguien puede citarte. Esta publicación está pendiente mientras obtenemos la aprobación del autor original.",
@@ -926,8 +936,8 @@
"status.quote_private": "Las publicaciones privadas no pueden citarse",
"status.quotes": "{count, plural,one {cita} other {citas}}",
"status.quotes.empty": "Nadie ha citado esta publicación todavía. Cuando alguien lo haga, aparecerá aquí.",
"status.quotes.local_other_disclaimer": "Las citas rechazadas por el autor no se mostrarán.",
"status.quotes.remote_other_disclaimer": "Solo se garantiza que se muestren las citas de {domain}. Las citas rechazadas por el autor no se mostrarán.",
"status.quotes.local_other_disclaimer": "Las citas rechazadas pro el autor no serán mostradas.",
"status.quotes.remote_other_disclaimer": "Solo las citas hechas por {domain} están garantizadas a ser vistas aquí. Las citas rechazadas por el autor no serán mostradas.",
"status.read_more": "Leer más",
"status.reblog": "Impulsar",
"status.reblog_or_quote": "Impulsar o citar",
@@ -1008,6 +1018,8 @@
"video.volume_down": "Bajar el volumen",
"video.volume_up": "Subir el volumen",
"visibility_modal.button_title": "Establece la visibilidad",
"visibility_modal.direct_quote_warning.text": "Si guardas la siguiente configuración, se mostrará un enlace en vez de la cita incrustada.",
"visibility_modal.direct_quote_warning.title": "No se pueden incrustar citas en menciones privadas",
"visibility_modal.header": "Visibilidad e interacción",
"visibility_modal.helper.direct_quoting": "Las menciones privadas creadas en Mastodon no pueden ser citadas por otros.",
"visibility_modal.helper.privacy_editing": "La visibilidad no se puede cambiar después de que se haya hecho una publicación.",

View File

@@ -194,6 +194,7 @@
"community.column_settings.local_only": "Solo local",
"community.column_settings.media_only": "Solo multimedia",
"community.column_settings.remote_only": "Solo remoto",
"compose.error.blank_post": "El mensaje no puede estar en blanco.",
"compose.language.change": "Cambiar idioma",
"compose.language.search": "Buscar idiomas...",
"compose.published.body": "Publicado.",
@@ -246,6 +247,11 @@
"confirmations.missing_alt_text.secondary": "Publicar de todos modos",
"confirmations.missing_alt_text.title": "¿Deseas añadir texto alternativo?",
"confirmations.mute.confirm": "Silenciar",
"confirmations.private_quote_notify.cancel": "Volver a la edición",
"confirmations.private_quote_notify.confirm": "Publicar",
"confirmations.private_quote_notify.do_not_show_again": "No mostrar este mensaje de nuevo",
"confirmations.private_quote_notify.message": "La persona a la que estás citando y otras mencionadas serán notificadas y podrán ver tu publicación, incluso si no te siguen.",
"confirmations.private_quote_notify.title": "¿Compartir con seguidores y usuarios mencionados?",
"confirmations.quiet_post_quote_info.dismiss": "No me lo vuelvas a recordar",
"confirmations.quiet_post_quote_info.got_it": "Entendido",
"confirmations.quiet_post_quote_info.message": "Cuando cites una publicación pública silenciosa, tu publicación se ocultará de las cronologías de tendencias.",
@@ -758,6 +764,7 @@
"privacy_policy.title": "Política de Privacidad",
"quote_error.edit": "No se pueden añadir citas cuando se edita una publicación.",
"quote_error.poll": "No es posible citar encuestas.",
"quote_error.private_mentions": "No se permite citar con menciones privadas.",
"quote_error.quote": "Solo se permite una cita a la vez.",
"quote_error.unauthorized": "No tienes permiso para citar esta publicación.",
"quote_error.upload": "No se permite citar con archivos multimedia.",
@@ -911,9 +918,12 @@
"status.pin": "Fijar",
"status.quote": "Citar",
"status.quote.cancel": "Cancelar cita",
"status.quote_error.blocked_account_hint.title": "Esta publicación está oculta porque has bloqueado a @{name}.",
"status.quote_error.blocked_domain_hint.title": "Esta publicación está oculta porque has bloqueado @{domain}.",
"status.quote_error.filtered": "Oculto debido a uno de tus filtros",
"status.quote_error.limited_account_hint.action": "Mostrar de todos modos",
"status.quote_error.limited_account_hint.title": "Esta cuenta ha sido ocultada por los moderadores de {domain}.",
"status.quote_error.muted_account_hint.title": "Esta publicación está oculta porque has silenciado a @{name}.",
"status.quote_error.not_available": "Publicación no disponible",
"status.quote_error.pending_approval": "Publicación pendiente",
"status.quote_error.pending_approval_popout.body": "En Mastodon, puedes controlar si alguien puede citarte. Esta publicación está pendiente mientras obtenemos la aprobación del autor original.",
@@ -1008,6 +1018,8 @@
"video.volume_down": "Bajar volumen",
"video.volume_up": "Subir volumen",
"visibility_modal.button_title": "Configura la visibilidad",
"visibility_modal.direct_quote_warning.text": "Si guardas la configuración actual, la cita incrustada se convertirá en un enlace.",
"visibility_modal.direct_quote_warning.title": "No se pueden incluir citas en menciones privadas",
"visibility_modal.header": "Visibilidad e interacciones",
"visibility_modal.helper.direct_quoting": "Las menciones privadas publicadas en Mastodon no pueden ser citadas por otros usuarios.",
"visibility_modal.helper.privacy_editing": "La visibilidad no se puede cambiar después de que se haya hecho una publicación.",

View File

@@ -64,10 +64,10 @@
"account.media": "Meedia",
"account.mention": "Maini @{name}",
"account.moved_to": "{name} on teada andnud, et ta uus konto on nüüd:",
"account.mute": "Vaigista @{name}",
"account.mute_notifications_short": "Vaigista teavitused",
"account.mute_short": "Vaigista",
"account.muted": "Vaigistatud",
"account.mute": "Summuta @{name}",
"account.mute_notifications_short": "Summuta teavitused",
"account.mute_short": "Summuta",
"account.muted": "Summutatud",
"account.muting": "Summutatud konto",
"account.mutual": "Te jälgite teineteist",
"account.no_bio": "Kirjeldust pole lisatud.",
@@ -87,9 +87,9 @@
"account.unblock_short": "Eemalda blokeering",
"account.unendorse": "Ära kuva profiilil",
"account.unfollow": "Jälgid",
"account.unmute": "Ära vaigista @{name}",
"account.unmute_notifications_short": "Tühista teadete vaigistamine",
"account.unmute_short": "Lõpeta vaigistamine",
"account.unmute": "Lõpeta {name} kasutaja summutamine",
"account.unmute_notifications_short": "Lõpeta teavituste summutamine",
"account.unmute_short": "Lõpeta summutamine",
"account_note.placeholder": "Klõpsa märke lisamiseks",
"admin.dashboard.daily_retention": "Kasutajate päevane allesjäämine peale registreerumist",
"admin.dashboard.monthly_retention": "Kasutajate kuine allesjäämine peale registreerumist",
@@ -173,11 +173,13 @@
"column.edit_list": "Muuda loendit",
"column.favourites": "Lemmikud",
"column.firehose": "Postitused reaalajas",
"column.firehose_local": "Selle serveri sisuvoog reaalajas",
"column.firehose_singular": "Postitused reaalajas",
"column.follow_requests": "Jälgimistaotlused",
"column.home": "Kodu",
"column.list_members": "Halda loendi liikmeid",
"column.lists": "Loetelud",
"column.mutes": "Vaigistatud kasutajad",
"column.mutes": "Summutatud kasutajad",
"column.notifications": "Teated",
"column.pins": "Kinnitatud postitused",
"column.public": "Föderatiivne ajajoon",
@@ -192,6 +194,7 @@
"community.column_settings.local_only": "Ainult kohalik",
"community.column_settings.media_only": "Ainult meedia",
"community.column_settings.remote_only": "Ainult kaug",
"compose.error.blank_post": "Postitus ei saa jääda tühjaks.",
"compose.language.change": "Muuda keelt",
"compose.language.search": "Otsi keeli...",
"compose.published.body": "Postitus tehtud.",
@@ -243,7 +246,12 @@
"confirmations.missing_alt_text.message": "Sinu postituses on ilma alt-tekstita meediat. Kirjelduse lisamine aitab su sisu muuta ligipääsetavaks rohkematele inimestele.",
"confirmations.missing_alt_text.secondary": "Postita siiski",
"confirmations.missing_alt_text.title": "Lisada alt-tekst?",
"confirmations.mute.confirm": "Vaigista",
"confirmations.mute.confirm": "Summuta",
"confirmations.private_quote_notify.cancel": "Tagasi muutmise juurde",
"confirmations.private_quote_notify.confirm": "Avalda postitus",
"confirmations.private_quote_notify.do_not_show_again": "Ära kuva enam seda sõnumit uuesti",
"confirmations.private_quote_notify.message": "Nii see, keda sa tsiteerid, kui need, keda mainid, saavad asjakohase teavituse ja võivad vaadata sinu postitust ka siis, kui nad pole sinu jälgijad.",
"confirmations.private_quote_notify.title": "Kas jagad jälgijate ja mainitud kasutajatega?",
"confirmations.quiet_post_quote_info.dismiss": "Ära tuleta enam meelde",
"confirmations.quiet_post_quote_info.got_it": "Sain aru",
"confirmations.quiet_post_quote_info.message": "Vaikse, aga avaliku postituse tsiteerimisel sinu postitus on peidetud populaarsust koguvatel ajajoontel.",
@@ -287,7 +295,7 @@
"domain_block_modal.they_can_interact_with_old_posts": "Inimesed sellest serverist saavad suhestuda sinu vanade postitustega.",
"domain_block_modal.they_cant_follow": "Sellest serverist ei saa keegi sind jälgida.",
"domain_block_modal.they_wont_know": "Nad ei tea, et nad on blokeeritud.",
"domain_block_modal.title": "Blokeerida domeen?",
"domain_block_modal.title": "Kas blokeerid domeeni?",
"domain_block_modal.you_will_lose_num_followers": "Sult kaob {followersCount, plural, one {{followersCountDisplay} jälgija} other {{followersCountDisplay} jälgijat}} ja {followingCount, plural, one {{followingCountDisplay} inimene} other {{followingCountDisplay} inimest}}, keda sa ise jälgid.",
"domain_block_modal.you_will_lose_relationships": "Sa kaotad kõik oma jälgijad ja inimesed, kes sind jälgivad sellest serverist.",
"domain_block_modal.you_wont_see_posts": "Sa ei näe selle serveri kasutajate postitusi ega teavitusi.",
@@ -343,7 +351,7 @@
"empty_column.hashtag": "Selle sildi all ei ole ühtegi postitust.",
"empty_column.home": "Su koduajajoon on tühi. Jälgi rohkemaid inimesi, et seda täita {suggestions}",
"empty_column.list": "Siin loetelus pole veel midagi. Kui loetelu liikmed teevad uusi postitusi, näed neid siin.",
"empty_column.mutes": "Sa pole veel ühtegi kasutajat vaigistanud.",
"empty_column.mutes": "Sa pole veel ühtegi kasutajat summutanud.",
"empty_column.notification_requests": "Kõik tühi! Siin pole mitte midagi. Kui saad uusi teavitusi, ilmuvad need siin vastavalt sinu seadistustele.",
"empty_column.notifications": "Ei ole veel teateid. Kui keegi suhtleb sinuga, näed seda siin.",
"empty_column.public": "Siin pole midagi! Kirjuta midagi avalikku või jälgi ise kasutajaid täitmaks seda ruumi",
@@ -431,7 +439,7 @@
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} postitust} other {{counter} postitust}} täna",
"hashtag.feature": "Tõsta profiilis esile",
"hashtag.follow": "Jälgi silti",
"hashtag.mute": "Vaigista @#{hashtag}",
"hashtag.mute": "Summuta teemaviide @#{hashtag}",
"hashtag.unfeature": "Ära tõsta profiilis esile",
"hashtag.unfollow": "Lõpeta sildi jälgimine",
"hashtags.and_other": "…ja {count, plural, one {}other {# veel}}",
@@ -491,7 +499,7 @@
"keyboard_shortcuts.load_more": "Fookus „Laadi veel“ nupule",
"keyboard_shortcuts.local": "Ava kohalik ajajoon",
"keyboard_shortcuts.mention": "Maini autorit",
"keyboard_shortcuts.muted": "Ava vaigistatud kasutajate loetelu",
"keyboard_shortcuts.muted": "Ava summutatud kasutajate loetelu",
"keyboard_shortcuts.my_profile": "Ava oma profiil",
"keyboard_shortcuts.notifications": "Ava teadete veerg",
"keyboard_shortcuts.open_media": "Ava meedia",
@@ -552,11 +560,11 @@
"moved_to_account_banner.text": "Kontot {disabledAccount} ei ole praegu võimalik kasutada, sest kolisid kontole {movedToAccount}.",
"mute_modal.hide_from_notifications": "Peida teavituste hulgast",
"mute_modal.hide_options": "Peida valikud",
"mute_modal.indefinite": "Kuni eemaldan neilt vaigistuse",
"mute_modal.indefinite": "Kuni eemaldan neilt summutamise",
"mute_modal.show_options": "Kuva valikud",
"mute_modal.they_can_mention_and_follow": "Ta saab sind mainida ja sind jälgida, kuid sa ei näe teda.",
"mute_modal.they_wont_know": "Ta ei tea, et ta on vaigistatud.",
"mute_modal.title": "Vaigistada kasutaja?",
"mute_modal.they_wont_know": "Ta ei tea, et ta on summutatud.",
"mute_modal.title": "Kas summutad kasutaja?",
"mute_modal.you_wont_see_mentions": "Sa ei näe postitusi, mis teda mainivad.",
"mute_modal.you_wont_see_posts": "Ta näeb jätkuvalt sinu postitusi, kuid sa ei näe tema omi.",
"navigation_bar.about": "Teave",
@@ -569,7 +577,7 @@
"navigation_bar.direct": "Privaatsed mainimised",
"navigation_bar.domain_blocks": "Peidetud domeenid",
"navigation_bar.favourites": "Lemmikud",
"navigation_bar.filters": "Vaigistatud sõnad",
"navigation_bar.filters": "Summutatud sõnad",
"navigation_bar.follow_requests": "Jälgimistaotlused",
"navigation_bar.followed_tags": "Jälgitavad märksõnad",
"navigation_bar.follows_and_followers": "Jälgitavad ja jälgijad",
@@ -580,7 +588,7 @@
"navigation_bar.logout": "Logi välja",
"navigation_bar.moderation": "Modereerimine",
"navigation_bar.more": "Lisavalikud",
"navigation_bar.mutes": "Vaigistatud kasutajad",
"navigation_bar.mutes": "Summutatud kasutajad",
"navigation_bar.opened_in_classic_interface": "Postitused, kontod ja teised spetsiaalsed lehed avatakse vaikimisi klassikalises veebiliideses.",
"navigation_bar.preferences": "Eelistused",
"navigation_bar.privacy_and_reach": "Privaatsus ja ulatus",
@@ -756,6 +764,7 @@
"privacy_policy.title": "Isikuandmete kaitse",
"quote_error.edit": "Postituse muutmisel ei saa tsitaati lisada.",
"quote_error.poll": "Tsiteerimine pole küsitlustes lubatud.",
"quote_error.private_mentions": "Tsiteerimine pole otsemainimiste puhul lubatud.",
"quote_error.quote": "Korraga on lubatud vaid üks tsitaat.",
"quote_error.unauthorized": "Sul pole õigust seda postitust tsiteerida.",
"quote_error.upload": "Tsiteerimine pole manuste puhul lubatud.",
@@ -794,8 +803,8 @@
"report.comment.title": "Kas arvad, et on veel midagi, mida me peaks teadma?",
"report.forward": "Edasta ka {target} domeeni",
"report.forward_hint": "See kasutaja on teisest serverist. Kas saadan anonümiseeritud koopia sellest teatest sinna ka?",
"report.mute": "Vaigista",
"report.mute_explanation": "Sa ei näe tema postitusi. Ta võib ikka sind jälgida ja su postitusi näha. Ta ei saa teada, et ta on vaigistatud.",
"report.mute": "Summuta",
"report.mute_explanation": "Sa ei näe tema postitusi. Ta võib ikka sind jälgida ja su postitusi näha. Ta ei saa teada, et ta on summutatud.",
"report.next": "Järgmine",
"report.placeholder": "Lisaks kommentaarid",
"report.reasons.dislike": "Mulle ei meeldi see",
@@ -903,15 +912,18 @@
"status.media_hidden": "Meedia peidetud",
"status.mention": "Maini @{name}'i",
"status.more": "Veel",
"status.mute": "Vaigista @{name}",
"status.mute_conversation": "Vaigista vestlus",
"status.mute": "Summuta @{name}",
"status.mute_conversation": "Summuta vestlus",
"status.open": "Laienda postitus",
"status.pin": "Kinnita profiilile",
"status.quote": "Tsiteeri",
"status.quote.cancel": "Katkesta tsiteerimine",
"status.quote_error.blocked_account_hint.title": "Kuna sa oled blokeerinud kasutaja @{name}, siis see postitus on peidetud.",
"status.quote_error.blocked_domain_hint.title": "Kuna sa oled blokeerinud domeeni @{domain}, siis see postitus on peidetud.",
"status.quote_error.filtered": "Peidetud mõne kasutatud filtri tõttu",
"status.quote_error.limited_account_hint.action": "Näita ikkagi",
"status.quote_error.limited_account_hint.title": "See profiil on peidetud {domain} serveri moderaatorite poolt.",
"status.quote_error.muted_account_hint.title": "Kuna sa oled summutanud kasutaja @{name}, siis see postitus on peidetud.",
"status.quote_error.not_available": "Postitus pole saadaval",
"status.quote_error.pending_approval": "Postitus on ootel",
"status.quote_error.pending_approval_popout.body": "Mastodonis saad sa kontrollida seda, kes võib sind tsiteerida. See postitus on seni ootel, kuni pole algse autori kinnitust tsiteerimisele.",
@@ -953,7 +965,7 @@
"status.translate": "Tõlgi",
"status.translated_from_with": "Tõlgitud {lang} keelest kasutades teenust {provider}",
"status.uncached_media_warning": "Eelvaade pole saadaval",
"status.unmute_conversation": "Ära vaigista vestlust",
"status.unmute_conversation": "Lõpeta vestluse summutamine",
"status.unpin": "Eemalda profiilile kinnitus",
"subscribed_languages.lead": "Pärast muudatust näed koduvaates ja loetelude ajajoontel postitusi valitud keeltes. Ära vali midagi, kui tahad näha postitusi kõikides keeltes.",
"subscribed_languages.save": "Salvesta muudatused",
@@ -997,15 +1009,17 @@
"video.expand": "Suurenda video",
"video.fullscreen": "Täisekraan",
"video.hide": "Peida video",
"video.mute": "Vaigista",
"video.mute": "Summuta",
"video.pause": "Paus",
"video.play": "Mängi",
"video.skip_backward": "Keri tagasi",
"video.skip_forward": "Keri edasi",
"video.unmute": "Lõpeta vaigistamine",
"video.unmute": "Lõpeta summutamine",
"video.volume_down": "Heli vaiksemaks",
"video.volume_up": "Heli valjemaks",
"visibility_modal.button_title": "Muuda nähtavust",
"visibility_modal.direct_quote_warning.text": "Kui sa need seadistused salvestad, siis lõimitud tsitaat muutub lingiks.",
"visibility_modal.direct_quote_warning.title": "Tsitaate ei saa privaatse mainimise puhul lõimida",
"visibility_modal.header": "Nähtavus ja kasutus",
"visibility_modal.helper.direct_quoting": "Ainult mainituile mõeldud Mastodoni postitusi ei saa teiste poolt tsiteerida.",
"visibility_modal.helper.privacy_editing": "Nähtavust ei saa peale postituse avaldamist muuta.",

View File

@@ -28,6 +28,7 @@
"account.disable_notifications": "Utzi jakinarazteari @{name} erabiltzaileak argitaratzean",
"account.domain_blocking": "Eragotzitako domeinua",
"account.edit_profile": "Editatu profila",
"account.edit_profile_short": "Editatu",
"account.enable_notifications": "Jakinarazi @{name} erabiltzaileak argitaratzean",
"account.endorse": "Nabarmendu profilean",
"account.familiar_followers_many": "Jarraitzaileak: {name1}, {name2} eta beste {othersCount, plural, one {ezagun bat} other {# ezagun}}",
@@ -40,6 +41,11 @@
"account.featured_tags.last_status_never": "Bidalketarik ez",
"account.follow": "Jarraitu",
"account.follow_back": "Jarraitu bueltan",
"account.follow_back_short": "Jarraitu bueltan",
"account.follow_request": "Eskatu jarraitzeko",
"account.follow_request_cancel": "Ezeztatu eskaera",
"account.follow_request_cancel_short": "Ezeztatu",
"account.follow_request_short": "Eskaera",
"account.followers": "Jarraitzaileak",
"account.followers.empty": "Ez du inork erabiltzaile hau jarraitzen oraindik.",
"account.followers_counter": "{count, plural, one {{counter} jarraitzaile} other {{counter} jarraitzaile}}",
@@ -107,6 +113,11 @@
"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:",
@@ -162,6 +173,8 @@
"column.edit_list": "Editatu zerrenda",
"column.favourites": "Gogokoak",
"column.firehose": "Zuzeneko jarioak",
"column.firehose_local": "Zerbitzari honen zuzeneko jarioa",
"column.firehose_singular": "Zuzeneko jarioa",
"column.follow_requests": "Jarraitzeko eskaerak",
"column.home": "Hasiera",
"column.list_members": "Kudeatu zerrrendako partaideak",
@@ -181,6 +194,7 @@
"community.column_settings.local_only": "Lokala soilik",
"community.column_settings.media_only": "Edukiak soilik",
"community.column_settings.remote_only": "Urrunekoa soilik",
"compose.error.blank_post": "Bidalketa ezin da hutsik egon.",
"compose.language.change": "Aldatu hizkuntza",
"compose.language.search": "Bilatu hizkuntzak...",
"compose.published.body": "Argitalpena argitaratuta.",
@@ -233,13 +247,30 @@
"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.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.",
"confirmations.private_quote_notify.title": "Partekatu jarraitzaileekin eta aipatutako erabiltzaileekin?",
"confirmations.quiet_post_quote_info.dismiss": "Ez gogorarazi berriro",
"confirmations.quiet_post_quote_info.got_it": "Ulertuta",
"confirmations.quiet_post_quote_info.message": "Deiadar urriko bidalketa bat aipatzen duzunean, zure bidalketa joeretatik ezkutatuko da.",
"confirmations.quiet_post_quote_info.title": "Deiadar urriko bidalketaren aipua",
"confirmations.redraft.confirm": "Ezabatu eta berridatzi",
"confirmations.redraft.message": "Ziur argitalpen hau ezabatu eta zirriborroa berriro egitea nahi duzula? Gogokoak eta bultzadak galduko dira, eta jatorrizko argitalpenaren erantzunak zurtz geratuko dira.",
"confirmations.redraft.title": "Ezabatu eta berridatzi bidalketa?",
"confirmations.remove_from_followers.confirm": "Jarraitzailea Kendu",
"confirmations.remove_from_followers.message": "{name}-k zu jarraitzeari utziko dio. Seguru zaude jarraitu nahi duzula?",
"confirmations.remove_from_followers.title": "Jarraitzailea kendu nahi duzu?",
"confirmations.revoke_quote.confirm": "Ezabatu bidalketa",
"confirmations.revoke_quote.message": "Ekintza hau ezin da desegin.",
"confirmations.revoke_quote.title": "Ezabatu bidalketa?",
"confirmations.unblock.confirm": "Desblokeatu",
"confirmations.unblock.title": "Desblokeatu {name}?",
"confirmations.unfollow.confirm": "Utzi jarraitzeari",
"confirmations.unfollow.title": "{name} jarraitzeari utzi?",
"confirmations.withdraw_request.confirm": "Baztertu eskaera",
"confirmations.withdraw_request.title": "Baztertu {name} jarraitzeko eskaera?",
"content_warning.hide": "Tuta ezkutatu",
"content_warning.show": "Erakutsi hala ere",
"content_warning.show_more": "Erakutsi gehiago",
@@ -265,6 +296,7 @@
"domain_block_modal.they_cant_follow": "Zerbitzari honetako inork ezin zaitu jarraitu.",
"domain_block_modal.they_wont_know": "Ez dute jakingo blokeatuak izan direnik.",
"domain_block_modal.title": "Domeinua blokeatu nahi duzu?",
"domain_block_modal.you_will_lose_num_followers": "{followersCount, plural, one {Jarraitzaile {followersCountDisplay}} other {{followersCountDisplay} jarraitzaile}} eta {followingCount, plural, one {jarraitzen duzun pertsona {followingCountDisplay}} other {jarraitzen dituzun beste {followingCountDisplay} pertsona}} galduko dituzu.",
"domain_block_modal.you_will_lose_relationships": "Instantzia honetatik jarraitzen dituzun jarraitzaile eta pertsona guztiak galduko dituzu.",
"domain_block_modal.you_wont_see_posts": "Ez dituzu zerbitzari honetako erabiltzaileen argitalpenik edota jakinarazpenik ikusiko.",
"domain_pill.activitypub_lets_connect": "Mastodon-en ez ezik, beste sare sozialen aplikazioetako jendearekin konektatzea eta harremanetan jartzea uzten dizu.",
@@ -280,6 +312,7 @@
"domain_pill.your_handle": "Zure helbidea:",
"domain_pill.your_server": "Zure etxe digitala, non zure bidalketak dauden. Ez al zaizu gustatzen? Transferitu zerbitzariak edonoiz eta ekarri zure jarraitzaileak ere.",
"domain_pill.your_username": "Zerbitzarian duzun identifikatzaile bakarra. Baliteke erabiltzaile-izen bera duten erabiltzaileak zerbitzari desberdinetan aurkitzea.",
"dropdown.empty": "Aukeratu bat",
"embed.instructions": "Txertatu bidalketa hau zure webgunean beheko kodea kopiatuz.",
"embed.preview": "Hau da izango duen itxura:",
"emoji_button.activity": "Jarduera",
@@ -308,6 +341,7 @@
"empty_column.bookmarked_statuses": "Oraindik ez dituzu bidalketa laster-markatutarik. Bat laster-markatzerakoan, hemen agertuko da.",
"empty_column.community": "Denbora-lerro lokala hutsik dago. Idatzi zerbait publikoki pilota biraka jartzeko!",
"empty_column.direct": "Ez duzu aipamen pribaturik oraindik. Baten bat bidali edo jasotzen duzunean, hemen agertuko da.",
"empty_column.disabled_feed": "Zure zerbitzariko administratzaileek jario hau desgaitu dute.",
"empty_column.domain_blocks": "Ez dago ezkutatutako domeinurik oraindik.",
"empty_column.explore_statuses": "Ez dago joerarik une honetan. Begiratu beranduago!",
"empty_column.favourited_statuses": "Ez duzu gogokorik oraindik. Gogoko bat duzunean, hemen agertuko da.",
@@ -332,6 +366,7 @@
"explore.trending_links": "Berriak",
"explore.trending_statuses": "Tutak",
"explore.trending_tags": "Traolak",
"featured_carousel.header": "{count, plural, one {Finkatutako sarrera} other {Finkatutako sarrerak}}",
"featured_carousel.next": "Hurrengoa",
"featured_carousel.post": "Argitaratu",
"featured_carousel.previous": "Aurrekoa",
@@ -435,10 +470,12 @@
"ignore_notifications_modal.private_mentions_title": "Eskatu gabeko aipamen pribatuen jakinarazpenei ez ikusiarena egin?",
"info_button.label": "Laguntza",
"info_button.what_is_alt_text": "<h1>Zer da Alt testua?</h1><p>Alt testuak irudiak deskribatzeko aukera ematen du, ikusmen-urritasunak, banda-zabalera txikiko konexioak edo testuinguru gehigarria nahi duten pertsonentzat.</p><p>Alt testu argi, zehatz eta objektiboen bidez, guztion irisgarritasuna eta ulermena hobetu ditzakezu.</p><ul><li>Hartu elementu garrantzitsuenak</li><li>Laburbildu irudietako testua</li><li>Erabili esaldien egitura erregularra</li><li>Baztertu informazio erredundantea.</li><li>Enfokatu joeretan eta funtsezko elementuetan irudi konplexuetan (diagrametan edo mapetan, adibidez)</li></ul>",
"interaction_modal.action": "{name} erabiltzailearen sarrerarekin interaktuatzeko, saioa hasi behar duzu erabiltzen duzun Mastodon zerbitzarian.",
"interaction_modal.go": "Joan",
"interaction_modal.no_account_yet": "Ez al duzu konturik oraindik?",
"interaction_modal.on_another_server": "Beste zerbitzari batean",
"interaction_modal.on_this_server": "Zerbitzari honetan",
"interaction_modal.title": "Hasi saioa jarraitzeko",
"interaction_modal.username_prompt": "Adib. {example}",
"intervals.full.days": "{number, plural, one {egun #} other {# egun}}",
"intervals.full.hours": "{number, plural, one {ordu #} other {# ordu}}",
@@ -459,6 +496,7 @@
"keyboard_shortcuts.home": "hasierako denbora-lerroa irekitzeko",
"keyboard_shortcuts.hotkey": "Laster-tekla",
"keyboard_shortcuts.legend": "legenda hau bistaratzea",
"keyboard_shortcuts.load_more": "Fokuratu \"Kargatu gehiago\" botoia",
"keyboard_shortcuts.local": "denbora-lerro lokala irekitzeko",
"keyboard_shortcuts.mention": "egilea aipatzea",
"keyboard_shortcuts.muted": "mutututako erabiltzaileen zerrenda irekitzeko",
@@ -467,6 +505,7 @@
"keyboard_shortcuts.open_media": "Ireki edukia",
"keyboard_shortcuts.pinned": "Ireki finkatutako bidalketen zerrenda",
"keyboard_shortcuts.profile": "egilearen profila irekitzeko",
"keyboard_shortcuts.quote": "Aipatu sarrera",
"keyboard_shortcuts.reply": "Erantzun bidalketari",
"keyboard_shortcuts.requests": "Jarraitzeko eskaeren zerrenda irekia",
"keyboard_shortcuts.search": "bilaketan fokua jartzea",
@@ -478,6 +517,8 @@
"keyboard_shortcuts.translate": "bidalketa itzultzeko",
"keyboard_shortcuts.unfocus": "testua konposatzeko area / bilaketatik fokua kentzea",
"keyboard_shortcuts.up": "zerrendan gora mugitzea",
"learn_more_link.got_it": "Ulertuta",
"learn_more_link.learn_more": "Ikasi gehiago",
"lightbox.close": "Itxi",
"lightbox.next": "Hurrengoa",
"lightbox.previous": "Aurrekoa",
@@ -500,6 +541,7 @@
"lists.exclusive": "Ezkutatu kideak Hasieran",
"lists.exclusive_hint": "Norbait zerrenda honetan badago, ezkutatu zure Hasierako jariotik mezuak bi aldiz ez ikusteko.",
"lists.find_users_to_add": "Bilatu erabiltzaileak gehitzeko",
"lists.list_members_count": "{count, plural, one {kide #} other {# kide}}",
"lists.list_name": "Zerrenda izena",
"lists.new_list_name": "Zerrenda izen berria",
"lists.no_lists_yet": "Ez duzu zerrendarik oraindik.",
@@ -511,6 +553,7 @@
"lists.replies_policy.none": "Bat ere ez",
"lists.save": "Gorde",
"lists.search": "Bilatu",
"lists.show_replies_to": "Erakutsi zerrendako kideen erantzunak hauei:",
"load_pending": "{count, plural, one {elementu berri #} other {# elementu berri}}",
"loading_indicator.label": "Kargatzen…",
"media_gallery.hide": "Ezkutatu",
@@ -551,6 +594,10 @@
"navigation_bar.privacy_and_reach": "Pribatutasuna eta irismena",
"navigation_bar.search": "Bilatu",
"navigation_bar.search_trends": "Bilatu / Joera",
"navigation_panel.collapse_followed_tags": "Itxi jarraitzen dituzun traolen menua",
"navigation_panel.collapse_lists": "Itxi zerrenden menua",
"navigation_panel.expand_followed_tags": "Zabaldu jarraitzen dituzun traolen menua",
"navigation_panel.expand_lists": "Zabaldu zerrenden menua",
"not_signed_in_indicator.not_signed_in": "Baliabide honetara sarbidea izateko saioa hasi behar duzu.",
"notification.admin.report": "{name} erabiltzaileak {target} salatu du",
"notification.admin.report_account": "{name}-(e)k {target}-ren {count, plural, one {bidalketa bat} other {# bidalketa}} salatu zituen {category} delakoagatik",
@@ -559,15 +606,20 @@
"notification.admin.report_statuses_other": "{name} erabiltzaileak {target} salatu du",
"notification.admin.sign_up": "{name} erabiltzailea erregistratu da",
"notification.admin.sign_up.name_and_others": "{name} eta {count, plural, one {erabiltzaile # gehiago} other {# erabiltzaile gehiago}} erregistratu dira",
"notification.annual_report.message": "Zain duzu {year}(e)ko #Wrapstodon! Ikusi urteko zure unerik gogoangarrienak Mastodonen!",
"notification.annual_report.view": "Ikusi #Wrapstodon",
"notification.favourite": "{name}(e)k zure bidalketa gogoko du",
"notification.favourite.name_and_others_with_link": "{name} eta <a>{count, plural, one {erabiltzaile # gehiagok} other {# erabiltzaile gehiagok}}</a> zure bidalketa gogoko dute",
"notification.favourite_pm": "{name}-ek zure aipamen pribatua gogokoetan jarri du",
"notification.favourite_pm.name_and_others_with_link": "{name} erabiltzaileak eta beste <a>{count, plural, one {#ek} other {#(e)k}}</a> gogoko dute zure aipu pribatua",
"notification.follow": "{name}(e)k jarraitzen dizu",
"notification.follow.name_and_others": "{name} erabiltzaileak eta <a>{count, plural, one {# gehiagok} other {# gehiagok}}</a> jarraitu dizute",
"notification.follow_request": "{name}(e)k zu jarraitzeko eskaera egin du",
"notification.follow_request.name_and_others": "{name} eta {count, plural, one {erabiltzaile # gehiagok} other {# erabiltzaile gehiagok}} zu jarraitzeko eskaera egin dute",
"notification.label.mention": "Aipamena",
"notification.label.private_mention": "Aipamen pribatua",
"notification.label.private_reply": "Erantzun pribatua",
"notification.label.quote": "{name} erabiltzaileak zure bidalketa aipatu du",
"notification.label.reply": "Erantzuna",
"notification.mention": "Aipamena",
"notification.mentioned_you": "{name}(e)k aipatu zaitu",
@@ -582,18 +634,23 @@
"notification.moderation_warning.action_suspend": "Kontua itxi da.",
"notification.own_poll": "Zure inkesta amaitu da",
"notification.poll": "Zuk erantzun duzun inkesta bat bukatu da",
"notification.quoted_update": "{name} erabiltzaileak aipatu duzun post bat editatu du",
"notification.reblog": "{name}(e)k bultzada eman dio zure bidalketari",
"notification.reblog.name_and_others_with_link": "{name} eta <a>{count, plural, one {erabiltzaile # gehiagok} other {# erabiltzaile gehiagok}}</a> bultzada eman diote zure bidalketari",
"notification.relationships_severance_event": "{name} erabiltzailearekin galdutako konexioak",
"notification.relationships_severance_event.account_suspension": "{from} zerbitzariko administratzaile batek {target} bertan behera utzi du, hau da, ezin izango dituzu jaso hango eguneratzerik edo hangoekin elkarreragin.",
"notification.relationships_severance_event.domain_block": "{from} zerbitzariko administratzaile batek {target} blokeatu du, tartean zure {followersCount} jarraitzaile eta jarraitzen duzun {followingCount, plural, one {kontu #} other {# kontu}}.",
"notification.relationships_severance_event.learn_more": "Informazio gehiago",
"notification.relationships_severance_event.user_domain_block": "{target} blokeatu duzu, tartean zure {followersCount} jarraitzaile eta jarraitzen duzun {followingCount, plural, one {kontu #} other {# kontu}}.",
"notification.status": "{name} erabiltzaileak bidalketa egin berri du",
"notification.update": "{name} erabiltzaileak bidalketa bat editatu du",
"notification_requests.accept": "Onartu",
"notification_requests.accept_multiple": "{count, plural, one {Onartu eskaera…} other {Onartu # eskaerak…}}",
"notification_requests.confirm_accept_multiple.button": "{count, plural, one {Onartu eskaera} other {Onartu eskaerak}}",
"notification_requests.confirm_accept_multiple.message": "{count, plural, one {Jakinarazpen eskaera bat} other {# jakinarazpen eskaera}} onartzekotan zaude. Jarraitu nahi duzu?",
"notification_requests.confirm_accept_multiple.title": "Onartu jakinarazpen-eskaerak?",
"notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Baztertu eskaera} other {Baztertu eskaerak}}",
"notification_requests.confirm_dismiss_multiple.message": "{count, plural, one {Jakinarazpen eskaera bat} other {# jakinarazpen eskaera}} baztertzekotan zaude. Gerora ezingo {count, plural, one {duzu} other {dituzu}} berriz erraz atzitu. Jarraitu nahi duzu?",
"notification_requests.confirm_dismiss_multiple.title": "Baztertu jakinarazpen-eskaerak?",
"notification_requests.dismiss": "Baztertu",
"notification_requests.dismiss_multiple": "{count, plural, one {Baztertu eskaera…} other {Baztertu # eskaerak…}}",
@@ -621,6 +678,7 @@
"notifications.column_settings.mention": "Aipamenak:",
"notifications.column_settings.poll": "Inkestaren emaitzak:",
"notifications.column_settings.push": "Push jakinarazpenak",
"notifications.column_settings.quote": "Aipuak:",
"notifications.column_settings.reblog": "Bultzadak:",
"notifications.column_settings.show": "Erakutsi zutabean",
"notifications.column_settings.sound": "Jo soinua",
@@ -691,14 +749,25 @@
"poll_button.remove_poll": "Kendu inkesta",
"privacy.change": "Aldatu bidalketaren pribatutasuna",
"privacy.direct.long": "Argitalpen honetan aipatutako denak",
"privacy.direct.short": "Aipu pribatua",
"privacy.private.long": "Soilik jarraitzaileak",
"privacy.private.short": "Jarraitzaileak",
"privacy.public.long": "Mastodonen dagoen edo ez dagoen edonor",
"privacy.public.short": "Publikoa",
"privacy.quote.anyone": "{visibility}, edonork aipa dezake",
"privacy.quote.disabled": "{visibility}, aipuak desgaituta",
"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_policy.last_updated": "Azkenengo eguneraketa {date}",
"privacy_policy.title": "Pribatutasun politika",
"quote_error.edit": "Aipuak ezin dira gehitu bidalketa bat editatzean.",
"quote_error.poll": "Inkestak ezin dira aipatu.",
"quote_error.private_mentions": "Aipuak ez dira onartzen aipamen pribatuetan.",
"quote_error.quote": "Bidalketa bakoitzeko aipu bakarra onartzen da.",
"quote_error.unauthorized": "Ez duzu baimenik bidalketa hau aipatzeko.",
"quote_error.upload": "Aipuak ez dira onartzen multimedia eranskinekin.",
"recommended": "Gomendatua",
"refresh": "Berritu",
"regeneration_indicator.please_stand_by": "Itxaron, mesedez.",
@@ -714,6 +783,9 @@
"relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s",
"relative_time.today": "gaur",
"remove_quote_hint.button_label": "Ulertuta",
"remove_quote_hint.message": "{icon} aukeren menutik egin dezakezu.",
"remove_quote_hint.title": "Aipatu dizuten bidalketa kendu nahi duzu?",
"reply_indicator.attachments": "{count, plural, one {# eranskin} other {# eranskin}}",
"reply_indicator.cancel": "Utzi",
"reply_indicator.poll": "Inkesta",
@@ -788,8 +860,10 @@
"search_results.all": "Guztiak",
"search_results.hashtags": "Traolak",
"search_results.no_results": "Emaitzarik ez.",
"search_results.no_search_yet": "Saiatu bilatzen bidalketak, profilak edo traolak.",
"search_results.see_all": "Ikusi guztiak",
"search_results.statuses": "Bidalketak",
"search_results.title": "Bilatu \"{q}\"",
"server_banner.about_active_users": "Azken 30 egunetan zerbitzari hau erabili duen jendea (hilabeteko erabiltzaile aktiboak)",
"server_banner.active_users": "erabiltzaile aktibo",
"server_banner.administered_by": "Administratzailea(k):",
@@ -803,13 +877,23 @@
"status.admin_account": "Ireki @{name} erabiltzailearen moderazio interfazea",
"status.admin_domain": "{domain}-(r)en moderazio-interfazea ireki",
"status.admin_status": "Ireki bidalketa hau moderazio interfazean",
"status.all_disabled": "Bultzadak eta aipuak desgaituta daude",
"status.block": "Blokeatu @{name}",
"status.bookmark": "Laster-marka",
"status.cancel_reblog_private": "Kendu bultzada",
"status.cannot_quote": "Ez duzu bidalketa hau aipatzeko baimenik",
"status.cannot_reblog": "Bidalketa honi ezin zaio bultzada eman",
"status.contains_quote": "Aipua darama",
"status.context.loading": "Erantzun gehiago kargatzen",
"status.context.loading_error": "Ezin erantzun berririk kargatu",
"status.context.loading_success": "Erantzun berriak kargatuta",
"status.context.more_replies_found": "Erantzun gehiago aurkitu dira",
"status.context.retry": "Saiatu berriz",
"status.context.show": "Erakutsi",
"status.continued_thread": "Harian jarraitu zuen",
"status.copy": "Kopiatu bidalketaren esteka",
"status.delete": "Ezabatu",
"status.delete.success": "Bidalketa ezabatuta",
"status.detailed_status": "Elkarrizketaren ikuspegi xehetsua",
"status.direct": "Aipatu pribatuki @{name}",
"status.direct_indicator": "Aipamen pribatua",
@@ -832,18 +916,46 @@
"status.mute_conversation": "Mututu elkarrizketa",
"status.open": "Hedatu bidalketa hau",
"status.pin": "Finkatu profilean",
"status.quote": "Aipua",
"status.quote.cancel": "Utzi aipua",
"status.quote_error.blocked_account_hint.title": "Bidalketa hau ezkutatuta dago @{name} blokeatu duzulako.",
"status.quote_error.blocked_domain_hint.title": "Bidalketa hau ezkutatuta dago {domain} blokeatu duzulako.",
"status.quote_error.filtered": "Ezkutatuta zure iragazki baten ondorioz",
"status.quote_error.limited_account_hint.action": "Erakutsi hala ere",
"status.quote_error.limited_account_hint.title": "{domain} zerbitzariaren moderatzaileek kontu hau ezkutatu dute.",
"status.quote_error.muted_account_hint.title": "Bidalketa hau ezkutatuta dago @{name} mututu duzulako.",
"status.quote_error.not_available": "Bidalketa ez dago eskuragarri",
"status.quote_error.pending_approval": "Bidalketa zain dago",
"status.quote_error.pending_approval_popout.body": "Mastodonen, norbaitek aipa zaitzakeen kontrola dezakezu. Bidalketa hau argitaratzeke dago jatorrizko egilearen oniritzia jaso bitartean.",
"status.quote_error.revoked": "Egileak bidalketa kendu du",
"status.quote_followers_only": "Bidalketa hau jarraitzaileek soilik aipatu dezakete",
"status.quote_manual_review": "Egileak eskuz berrikusiko du",
"status.quote_noun": "Aipua",
"status.quote_policy_change": "Aldatu nork aipa zaitzakeen",
"status.quote_post_author": "@{name} erabiltzailearen bidalketaren aipua",
"status.quote_private": "Bidalketa pribatuak ezin dira aipatu",
"status.quotes": "{count, plural, one {aipu} other {aipu}}",
"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.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": "{count, plural, one {bultzada} other {bultzada}}",
"status.reblogs.empty": "Inork ez dio bultzada eman bidalketa honi oraindik. Inork egiten badu, hemen agertuko da.",
"status.redraft": "Ezabatu eta berridatzi",
"status.remove_bookmark": "Kendu laster-marka",
"status.remove_favourite": "Kendu gogokoetatik",
"status.remove_quote": "Kendu",
"status.replied_in_thread": "Harian erantzun zuen",
"status.replied_to": "{name} erabiltzaileari erantzuna",
"status.reply": "Erantzun",
"status.replyAll": "Erantzun harian",
"status.report": "Salatu @{name}",
"status.request_quote": "Eskatu aipatzeko",
"status.revoke_quote": "Ezabatu nire bidalketa @{name}-(r)en bidalketatik",
"status.sensitive_warning": "Kontuz: Eduki hunkigarria",
"status.share": "Partekatu",
"status.show_less_all": "Erakutsi denetarik gutxiago",
@@ -863,7 +975,9 @@
"tabs_bar.notifications": "Jakinarazpenak",
"tabs_bar.publish": "Bidalketa berria",
"tabs_bar.search": "Bilatu",
"terms_of_service.effective_as_of": "Indarrean {date}tik aurrera",
"terms_of_service.title": "Zerbitzuaren baldintzak",
"terms_of_service.upcoming_changes_on": "{date} datarako datozen aldaketak",
"time_remaining.days": "{number, plural, one {egun #} other {# egun}} amaitzeko",
"time_remaining.hours": "{number, plural, one {ordu #} other {# ordu}} amaitzeko",
"time_remaining.minutes": "{number, plural, one {minutu #} other {# minutu}} amaitzeko",
@@ -879,6 +993,12 @@
"upload_button.label": "Gehitu multimedia (JPEG, PNG, GIF, WebM, MP4, MOV)",
"upload_error.limit": "Fitxategi igoera muga gaindituta.",
"upload_error.poll": "Ez da inkestetan fitxategiak igotzea onartzen.",
"upload_error.quote": "Artxiboak igotzea ez da onartzen aipuekin.",
"upload_form.drag_and_drop.instructions": "Multimedia eranskin bat aukeratzeko, sakatu espazioa edo enter. Arrastatu bitartean, erabili gezi-teklak multimedia eranskina edozein norabidetan mugitzeko. Sakatu berriz espazioa edon enter multimedia eranskina bere kokapen berrian jartzeko, edo sakatu ESC uzteko.",
"upload_form.drag_and_drop.on_drag_cancel": "Arrastatzea bertan behera utzi da. {item} multimedia eranskina ez da mugitu.",
"upload_form.drag_and_drop.on_drag_end": "{item} multimedia eranskina ez da mugitu.",
"upload_form.drag_and_drop.on_drag_over": "{item} multimedia eranskina mugitu da.",
"upload_form.drag_and_drop.on_drag_start": "{item} multimedia eranskina hautatu da.",
"upload_form.edit": "Editatu",
"upload_progress.label": "Igotzen...",
"upload_progress.processing": "Prozesatzen…",
@@ -889,10 +1009,28 @@
"video.expand": "Hedatu bideoa",
"video.fullscreen": "Pantaila osoa",
"video.hide": "Ezkutatu bideoa",
"video.mute": "Mututu",
"video.pause": "Pausatu",
"video.play": "Jo",
"video.skip_backward": "Saltatu atzerantz",
"video.skip_forward": "Jauzi aurrerantz",
"video.unmute": "Soinua ezarri",
"video.volume_down": "Bolumena jaitsi",
"video.volume_up": "Bolumena Igo"
"video.volume_up": "Bolumena Igo",
"visibility_modal.button_title": "Ezarri ikusgarritasuna",
"visibility_modal.direct_quote_warning.text": "Uneko ezarpenak gordez gero, txertatutako aipua lotura bilakatuko da.",
"visibility_modal.direct_quote_warning.title": "Aipuak ezin dira sartu aipamen prbatuetan",
"visibility_modal.header": "Ikusgarritasuna eta elkarreraginak",
"visibility_modal.helper.direct_quoting": "Mastodonen argitaratutako aipamen pribatuak ezin dituzte beste erabiltzaileek aipatu.",
"visibility_modal.helper.privacy_editing": "Ikusgarritasuna ezin da aldatu bidalketa argitaratu ondoren.",
"visibility_modal.helper.privacy_private_self_quote": "Bidalketa pribatuen auto-aipuak ezin dira publiko egin.",
"visibility_modal.helper.private_quoting": "Jarraitzaileentzat soilik sortutako bidalketak Mastodonen ezin dituzte beste batzuek aipatu.",
"visibility_modal.helper.unlisted_quoting": "Jendeak aipatzen zaituenean, bere bidalketa ere joeren denbora-lerro publikoetatik ezkutatuko da.",
"visibility_modal.instructions": "Kontrolatu nork izan dezakeen elkarreragina bidalketa honekin. Ezarpenak etorkizuneko bidalketa guztiei ere aplika diezazkiekezu <link>Hobespenak > Bidalketarako lehentsitakoak</link> atalera joanda.",
"visibility_modal.privacy_label": "Ikusgarritasuna",
"visibility_modal.quote_followers": "Jarraitzaileentzat soilik",
"visibility_modal.quote_label": "Nork aipa dezake",
"visibility_modal.quote_nobody": "Nik bakarrik",
"visibility_modal.quote_public": "Edonork",
"visibility_modal.save": "Gorde"
}

View File

@@ -1,7 +1,7 @@
{
"about.blocks": "کارسازهای نظارت شده",
"about.blocks": "کارسازهای نظارت شده",
"about.contact": "تماس:",
"about.default_locale": "پیش‌فرض",
"about.default_locale": "پیش‌گزیده",
"about.disclaimer": "ماستودون نرم‌افزار آزاد و نشان تجاری یک شرکت غیر انتفاعی با مسئولیت محدود آلمانی است.",
"about.domain_blocks.no_reason_available": "دلیلی موجود نیست",
"about.domain_blocks.preamble": "ماستودون عموماً می‌گذارد محتوا را از از هر کارساز دیگری در دنیای شبکه‌های اجتماعی غیرمتمرکز دیده و با آنان برهم‌کنش داشته باشید. این‌ها استثناهایی هستند که روی این کارساز خاص وضع شده‌اند.",
@@ -28,18 +28,24 @@
"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 {# نفر دیگر از پی‌گرفته‌هایتان}}",
"account.familiar_followers_one": "پی‌گرفته از سوی {name1}",
"account.familiar_followers_two": "پی‌گرفته از سوی {name1} و {name2}",
"account.featured": " پیشنهادی",
"account.featured": "پیشنهادی",
"account.featured.accounts": "نمایه‌ها",
"account.featured.hashtags": "برچسب‌ها",
"account.featured_tags.last_status_at": "آخرین فرسته در {date}",
"account.featured_tags.last_status_never": "بدون فرسته",
"account.follow": "پی‌گرفتن",
"account.follow_back": "پی‌گیری متقابل",
"account.follow_back_short": "پی‌گیری متقابل",
"account.follow_request": "درخواست پی‌گیری",
"account.follow_request_cancel": "لغو درخواست",
"account.follow_request_cancel_short": "لغو",
"account.follow_request_short": "درخواست",
"account.followers": "پی‌گیرندگان",
"account.followers.empty": "هنوز کسی پی‌گیر این کاربر نیست.",
"account.followers_counter": "{count, plural, one {{counter} پی‌گیرنده} other {{counter} پی‌گیرنده}}",
@@ -167,6 +173,8 @@
"column.edit_list": "ویرایش سیاهه",
"column.favourites": "برگزیده‌ها",
"column.firehose": "خوراک‌های زنده",
"column.firehose_local": "خوراک زندهٔ این کارساز",
"column.firehose_singular": "خوراک زنده",
"column.follow_requests": "درخواست‌های پی‌گیری",
"column.home": "خانه",
"column.list_members": "مدیریت اعضای سیاهه",
@@ -186,6 +194,7 @@
"community.column_settings.local_only": "فقط محلی",
"community.column_settings.media_only": "فقط رسانه",
"community.column_settings.remote_only": "تنها دوردست",
"compose.error.blank_post": "فرسته نمی‌تواند خالی باشد.",
"compose.language.change": "تغییر زبان",
"compose.language.search": "جست‌وجوی زبان‌ها...",
"compose.published.body": "فرسته منتشر شد.",
@@ -238,6 +247,15 @@
"confirmations.missing_alt_text.secondary": "به هر حال پست کن",
"confirmations.missing_alt_text.title": "متن جایگزین اضافه شود؟",
"confirmations.mute.confirm": "خموش",
"confirmations.private_quote_notify.cancel": "بازگشت به ویرایش کردن",
"confirmations.private_quote_notify.confirm": "انتشار فرسته",
"confirmations.private_quote_notify.do_not_show_again": "دیگر این پیام نشان داده نشود",
"confirmations.private_quote_notify.message": "فردی که نقلش می‌کنید و کسانی که اشاره شده‌اند آگاه خواهند شد و می‌توانند فرسته‌تان را ببینند؛ حتا اگر پیتان نگیرند.",
"confirmations.private_quote_notify.title": "هم‌رسانی با پی‌گیرندگان و کاربران اشاره شده؟",
"confirmations.quiet_post_quote_info.dismiss": "دیگر یادآوری نشود",
"confirmations.quiet_post_quote_info.got_it": "گرفتم",
"confirmations.quiet_post_quote_info.message": "هنگام نقل کردن فرستهٔ عمومی ساکت، فرسته‌تان از خط‌های زمانی داغ پنهان خواهد بود.",
"confirmations.quiet_post_quote_info.title": "نقل کردن فرسته‌های عمومی ساکت",
"confirmations.redraft.confirm": "حذف و بازنویسی",
"confirmations.redraft.message": "مطمئنید که می‌خواهید این فرسته را حذف کنید و از نو بنویسید؟ با این کار تقویت‌ها و پسندهایش از دست رفته و پاسخ‌ها به آن بی‌مرجع می‌شود.",
"confirmations.redraft.title": "حذف و پیش‌نویسی دوبارهٔ فرسته؟",
@@ -247,7 +265,12 @@
"confirmations.revoke_quote.confirm": "حذف فرسته",
"confirmations.revoke_quote.message": "این اقدام قابل بازگشت نیست.",
"confirmations.revoke_quote.title": "آیا فرسته را حذف کنم؟",
"confirmations.unblock.confirm": "رفع انسداد",
"confirmations.unblock.title": "رفع انسداد {name}؟",
"confirmations.unfollow.confirm": "پی‌نگرفتن",
"confirmations.unfollow.title": "ناپی‌گیری {name}؟",
"confirmations.withdraw_request.confirm": "انصراف از درخواست",
"confirmations.withdraw_request.title": "انصراف از درخواست پی‌گیری {name}؟",
"content_warning.hide": "نهفتن فرسته",
"content_warning.show": "در هر صورت نشان داده شود",
"content_warning.show_more": "نمایش بیش‌تر",
@@ -318,6 +341,7 @@
"empty_column.bookmarked_statuses": "هنوز هیچ فرستهٔ نشانه‌گذاری شده‌ای ندارید. هنگامی که فرسته‌ای را نشانه‌گذاری کنید، این‌جا نشان داده خواهد شد.",
"empty_column.community": "خط زمانی محلی خالیست. چیزی نوشته تا چرخش بچرخد!",
"empty_column.direct": "هنوز هیچ اشاره خصوصی‌ای ندارید. هنگامی که چنین پیامی بگیرید یا بفرستید این‌جا نشان داده خواهد شد.",
"empty_column.disabled_feed": "این خوراک به دست مدیران کارسازتان از کار انداخته شده.",
"empty_column.domain_blocks": "هنوز هیچ دامنه‌ای مسدود نشده است.",
"empty_column.explore_statuses": "الآن چیزی پرطرفدار نیست. بعداً دوباره بررسی کنید!",
"empty_column.favourited_statuses": "شما هنوز هیچ فرسته‌ای را نپسندیده‌اید. هنگامی که فرسته‌ای را بپسندید، این‌جا نشان داده خواهد شد.",
@@ -446,10 +470,12 @@
"ignore_notifications_modal.private_mentions_title": "چشم‌پوشی از نام‌بری‌های خصوصی ناخواسته؟",
"info_button.label": "راهنما",
"info_button.what_is_alt_text": "<h1>متن جایگزین چیست؟</h1> <p>متن جایگزین توضیحات تصویری را برای افراد دارای اختلالات بینایی، اتصالات با پهنای باند کم یا کسانی که به دنبال زمینه اضافی هستند ارائه می دهد.</p> <p>با نوشتن متن جایگزین واضح، مختصر و عینی می توانید دسترسی و درک را برای همه بهبود بخشید.</p> <ul> <li>عناصر مهم را ضبط کنید</li> <li>متن را در تصاویر خلاصه کنید</li> <li>از ساختار جمله منظم استفاده کنید</li> <li>از اطلاعات اضافی خودداری کنید</li> <li>روی روندها و یافته های کلیدی در تصاویر پیچیده (مانند نمودارها یا نقشه ها) تمرکز کنید.</li> </ul>",
"interaction_modal.action": "برای تعامل با فرستهٔ {name} باید به حسابتان روی هر کارساز ماستودونی که استفاده می‌کنید وارد شوید.",
"interaction_modal.go": "برو",
"interaction_modal.no_account_yet": "هنوز حساب کاربری ندارید؟",
"interaction_modal.on_another_server": "روی کارسازی دیگر",
"interaction_modal.on_this_server": "روی این کارساز",
"interaction_modal.title": "ورود برای ادامه",
"interaction_modal.username_prompt": "به عنوان مثال {example}",
"intervals.full.days": "{number, plural, one {# روز} other {# روز}}",
"intervals.full.hours": "{number, plural, one {# ساعت} other {# ساعت}}",
@@ -470,6 +496,7 @@
"keyboard_shortcuts.home": "گشودن خط زمانی خانگی",
"keyboard_shortcuts.hotkey": "میان‌بر",
"keyboard_shortcuts.legend": "نمایش این نشانه",
"keyboard_shortcuts.load_more": "تمرکز روی دکمهٔ «بار کردن بیش‌تر»",
"keyboard_shortcuts.local": "گشودن خط زمانی محلی",
"keyboard_shortcuts.mention": "اشاره به نویسنده",
"keyboard_shortcuts.muted": "گشودن فهرست کاربران خموش",
@@ -478,6 +505,7 @@
"keyboard_shortcuts.open_media": "گشودن رسانه",
"keyboard_shortcuts.pinned": "گشودن سیاههٔ فرسته‌های سنجاق شده",
"keyboard_shortcuts.profile": "گشودن نمایهٔ نویسنده",
"keyboard_shortcuts.quote": "نقل فرسته",
"keyboard_shortcuts.reply": "پاسخ به فرسته",
"keyboard_shortcuts.requests": "گشودن سیاههٔ درخواست‌های پی‌گیری",
"keyboard_shortcuts.search": "تمرکز روی نوار جست‌وجو",
@@ -527,7 +555,7 @@
"lists.search": "جست‌وجو",
"lists.show_replies_to": "شامل پاسخ از اعضای لیست به",
"load_pending": "{count, plural, one {# مورد جدید} other {# مورد جدید}}",
"loading_indicator.label": "در حال بارگذاری…",
"loading_indicator.label": "بار کردن…",
"media_gallery.hide": "نهفتن",
"moved_to_account_banner.text": "حسابتان {disabledAccount} اکنون از کار افتاده؛ چرا که به {movedToAccount} منتقل شدید.",
"mute_modal.hide_from_notifications": "نهفتن از آگاهی‌ها",
@@ -582,8 +610,8 @@
"notification.annual_report.view": "دیدن #Wrapstodon",
"notification.favourite": "{name} فرسته‌تان را برگزید",
"notification.favourite.name_and_others_with_link": "{name} و <a>{count, plural, one {# نفر دیگر} other {# نفر دیگر}}</a> فرسته‌تان را برگزیدند",
"notification.favourite_pm": "{name} ذکر خصوصی شما را مورد علاقه قرار داد",
"notification.favourite_pm.name_and_others_with_link": "{name} و <a>{count, plural, one {دیگری} other {دیگران}}</a> ذکر خصوصی شما را مورد علاقه قرار دادند",
"notification.favourite_pm": "{name} اشارهٔ خصوصیتان را برگزید",
"notification.favourite_pm.name_and_others_with_link": "{name} و <a>{count, plural, one {# نفر دیگر} other {# نفر دیگر}}</a> اشارهٔ خصوصیتان را برگزیدند",
"notification.follow": "{name} پی‌گیرتان شد",
"notification.follow.name_and_others": "{name} و <a>{count, plural, other {#}} نفر دیگر</a> پیتان گرفتند",
"notification.follow_request": "{name} درخواست پی‌گیریتان را داد",
@@ -606,6 +634,7 @@
"notification.moderation_warning.action_suspend": "حسابتان معلّق شده.",
"notification.own_poll": "نظرسنجیتان پایان یافت",
"notification.poll": "نظرسنجی‌ای که در آن رأی دادید به پایان رسید",
"notification.quoted_update": "{name} فرسته‌ای که نقل کردید را ویراست",
"notification.reblog": "{name} فرسته‌تان را تقویت کرد",
"notification.reblog.name_and_others_with_link": "{name} و <a>{count, plural, one {# نفر دیگر} other {# نفر دیگر}}</a> فرسته‌تان را تقویت کردند",
"notification.relationships_severance_event": "قطع ارتباط با {name}",
@@ -618,10 +647,10 @@
"notification_requests.accept": "پذیرش",
"notification_requests.accept_multiple": "{count, plural, one {پذیرش درخواست…} other {پذیرش درخواست‌ها…}}",
"notification_requests.confirm_accept_multiple.button": "پذیرش {count, plural,one {درخواست} other {درخواست‌ها}}",
"notification_requests.confirm_accept_multiple.message": ر حال پذیرش {count, plural,one {یک}other {#}} درخواست آگاهی هستید. مطمئنید که می‌خواهید ادامه دهید؟",
"notification_requests.confirm_accept_multiple.message": ارید {count, plural,one {یک}other {#}} درخواست آگاهی را می‌پذیرید. مطمئنید که می‌خواهید ادامه دهید؟",
"notification_requests.confirm_accept_multiple.title": "پذیرش درخواست‌های آگاهی؟",
"notification_requests.confirm_dismiss_multiple.button": "رد {count, plural,one {درخواست} other {درخواست‌ها}}",
"notification_requests.confirm_dismiss_multiple.message": "شما در شرف رد کردن {count, plural, one {یک درخواست آگاهی} other {درخواست آگاهی}} هستید. دیگر نمی توانید به راحتی به {count, plural, one {آن} other {آن‌ها}} دسترسی پیدا کنید. آیا مطمئن هستید که می خواهید ادامه دهید؟",
"notification_requests.confirm_dismiss_multiple.message": "دارید {count, plural, one {یک درخواست آگاهی} other {# درخواست آگاهی}} را رد می‌کنید که دیگر نمی توانید به راحتی به {count, plural, one {آن} other {آن‌ها}} دسترسی پیدا کنید. مطمئنید که میخواهید ادامه دهید؟",
"notification_requests.confirm_dismiss_multiple.title": "رد کردن درخواست‌های آگاهی؟",
"notification_requests.dismiss": "دورانداختن",
"notification_requests.dismiss_multiple": "{count, plural, one {دورانداختن درخواست…} other {دورانداختن درخواست‌ها…}}",
@@ -720,19 +749,29 @@
"poll_button.remove_poll": "برداشتن نظرسنجی",
"privacy.change": "تغییر محرمانگی فرسته",
"privacy.direct.long": "هرکسی که در فرسته نام برده شده",
"privacy.direct.short": "ذکر خصوصی",
"privacy.direct.short": "اشارهٔ خصوصی",
"privacy.private.long": "تنها پی‌گیرندگانتان",
"privacy.private.short": "پی‌گیرندگان",
"privacy.public.long": "هرکسی در و بیرون از ماستودون",
"privacy.public.short": "عمومی",
"privacy.quote.anyone": "{visibility}، هرکسی می‌تواند نقل کند",
"privacy.quote.disabled": "{visibility}، نقل‌ها از کار افتاده",
"privacy.quote.limited": "{visibility}، نقل‌ها محدود شده",
"privacy.unlisted.additional": "درست مثل عمومی رفتار می‌کند؛ جز این که فرسته در برچسب‌ها یا خوراک‌های زنده، کشف یا جست‌وجوی ماستودون ظاهر نخواهد شد. حتا اگر کلیّت نمایه‌تان اجازه داده باشد.",
"privacy.unlisted.long": "نهفته از نتیجه‌های جست‌وجوی ماستودون و خط‌های زمانی داغ و عمومی",
"privacy.unlisted.short": "عمومی ساکت",
"privacy_policy.last_updated": "آخرین به‌روز رسانی در {date}",
"privacy_policy.title": "سیاست محرمانگی",
"quote_error.edit": "هنگام ویراستن فرسته نمی‌توان نقلی افزود.",
"quote_error.poll": "نقل نظرسنجی‌ها مجاز نیست.",
"quote_error.private_mentions": "نقل اشاره‌های مستقیم مجاز نیست.",
"quote_error.quote": "در هر زمان تنها یک نقل مجاز است.",
"quote_error.unauthorized": "مجاز به نقل این فرسته نیستید.",
"quote_error.upload": "نقل پیوست‌های رسانه‌ای مجاز نیست.",
"recommended": "پیشنهادشده",
"refresh": "نوسازی",
"regeneration_indicator.please_stand_by": "لطفا منتظر باشید.",
"regeneration_indicator.preparing_your_home_feed": "در حال آماده کردن خوراک خانگی شما…",
"regeneration_indicator.preparing_your_home_feed": "آماده کردن خوراک خانگیتان…",
"relative_time.days": "{number} روز",
"relative_time.full.days": "{number, plural, one {# روز} other {# روز}} پیش",
"relative_time.full.hours": "{number, plural, one {# ساعت} other {# ساعت}} پیش",
@@ -744,6 +783,9 @@
"relative_time.minutes": "{number} دقیقه",
"relative_time.seconds": "{number} ثانیه",
"relative_time.today": "امروز",
"remove_quote_hint.button_label": "گرفتم",
"remove_quote_hint.message": "می‌توانید این کار را از {icon} فهرست گزینه‌ها انجام دهید.",
"remove_quote_hint.title": "می‌خواهید فرستهٔ نقل شده‌تان را بردارید؟",
"reply_indicator.attachments": "{count, plural, one {# پیوست} other {# پیوست}}",
"reply_indicator.cancel": "لغو",
"reply_indicator.poll": "نظرسنجی",
@@ -780,9 +822,9 @@
"report.statuses.subtitle": "همهٔ موارد انجام شده را برگزینید",
"report.statuses.title": "آیا فرسته‌ای وجود دارد که از این گزارش پشتیبانی کند؟",
"report.submit": "فرستادن",
"report.target": "در حال گزارش {target}",
"report.target": "گزارش کردن {target}",
"report.thanks.take_action": "در اینجا گزینه‌هایی برای کنترل آنچه در ماستودون میبینید، وجود دارد:",
"report.thanks.take_action_actionable": "در حالی که ما این مورد را بررسی می‌کنیم، می‌توانید علیه @{name} اقدام کنید:",
"report.thanks.take_action_actionable": "تا بررسیش می‌کنیم می‌توانید علیه @{name} اقدام کنید:",
"report.thanks.title": "نمی‌خواهید این را ببینید؟",
"report.thanks.title_actionable": "ممنون بابت گزارش، ما آن را بررسی خواهیم کرد.",
"report.unfollow": "پی‌نگرفتن @{name}",
@@ -835,13 +877,23 @@
"status.admin_account": "گشودن واسط مدیریت برای @{name}",
"status.admin_domain": "گشودن واسط مدیریت برای {domain}",
"status.admin_status": "گشودن این فرسته در واسط مدیریت",
"status.all_disabled": "تقویت‌ها و نقل‌ها از کار افتاده‌اند",
"status.block": "انسداد @{name}",
"status.bookmark": "نشانک",
"status.cancel_reblog_private": "ناتقویت",
"status.cannot_quote": "مجاز به نقل این فرسته نیستید",
"status.cannot_reblog": "این فرسته قابل تقویت نیست",
"status.contains_quote": "دارای نقل",
"status.context.loading": "بار کردن پاسخ‌های بیش‌تر",
"status.context.loading_error": "نتوانست پاسخ‌های بیش‌تری بار کند",
"status.context.loading_success": "پاسخ‌های جدید بار شدند",
"status.context.more_replies_found": "پاسخ‌های بیش‌تری پیدا شد",
"status.context.retry": "تلاش دوباره",
"status.context.show": "نمایش",
"status.continued_thread": "رشتهٔ دنباله دار",
"status.copy": "رونوشت از پیوند فرسته",
"status.delete": "حذف",
"status.delete.success": "فرسته حذف شد",
"status.detailed_status": "نمایش کامل گفتگو",
"status.direct": "اشارهٔ خصوصی به @{name}",
"status.direct_indicator": "اشارهٔ خصوصی",
@@ -866,24 +918,43 @@
"status.pin": "سنجاق به نمایه",
"status.quote": "نقل‌قول",
"status.quote.cancel": "لغو نقل",
"status.quote_error.blocked_account_hint.title": "این فرسته نهفته است چرا که @{name} را مسدود کرده‌اید.",
"status.quote_error.blocked_domain_hint.title": "این فرسته نهفته است چرا که {domain} را مسدود کرده‌اید.",
"status.quote_error.filtered": "نهفته بنا بر یکی از پالایه‌هایتان",
"status.quote_error.limited_account_hint.action": "نمایش به هر روی",
"status.quote_error.limited_account_hint.title": "این حساب به دست ناظم‌های {domain} پنهان شده.",
"status.quote_error.muted_account_hint.title": "این فرسته نهفته است چرا که @{name} را خموش کرده‌اید.",
"status.quote_error.not_available": "فرسته در دسترس نیست",
"status.quote_error.pending_approval": "فرسته منتظر",
"status.quote_error.pending_approval_popout.body": "روی ماستودون می‌توان افراد مجاز به نقل را واپایید. این فرسته تا زمان گرفتن تأییده از نگارندهٔ اصلی معلّق است.",
"status.quote_error.revoked": "فرسته به دست نگارنده برداشته شد",
"status.quote_followers_only": "تنها پی‌گیران می‌توانند این فرسته را نقل کنند",
"status.quote_manual_review": "نگارنده به صورت دستی بررسی خواهد کرد",
"status.quote_noun": "نقل",
"status.quote_policy_change": "تغییر کسانی که می‌توانند نقل کنند",
"status.quote_post_author": "فرسته‌ای از @{name} نقل شد",
"status.quote_private": "فرسته‌های خصوصی نمی‌توانند نقل شوند",
"status.quotes": "{count, plural, one {نقل} other {نقل}}",
"status.quotes.empty": "هنوز کسی این فرسته را نقل نکرده. وقتی کسی چنین کند این‌جا نشان داده خواهد شد.",
"status.quotes.local_other_disclaimer": "نقل‌هایی که به دست نگارنده رد شده باشند نشان داده نخواهند شد.",
"status.quotes.remote_other_disclaimer": "تنها نقل‌ها از {domain} تضمین نمایش در این‌جا را دارند. نقل‌های رد شده به دست نگاره نشان داده نخواهند شد.",
"status.read_more": "بیشتر بخوانید",
"status.reblog": "تقویت",
"status.reblog_or_quote": "نقل یا تقویت",
"status.reblog_private": "هم‌رسانی دوباره با پی‌گیرانتان",
"status.reblogged_by": "{name} تقویت کرد",
"status.reblogs": "{count, plural, one {تقویت} other {تقویت}}",
"status.reblogs.empty": "هنوز هیچ کسی این فرسته را تقویت نکرده است. وقتی کسی چنین کاری کند، این‌جا نمایش داده خواهد شد.",
"status.redraft": "حذف و بازنویسی",
"status.remove_bookmark": "برداشتن نشانک",
"status.remove_favourite": "حذف از موارد دلخواه",
"status.remove_quote": "برداشتن",
"status.replied_in_thread": "در رشته پاسخ داده",
"status.replied_to": "به {name} پاسخ داد",
"status.reply": "پاسخ",
"status.replyAll": "پاسخ به رشته",
"status.report": "گزارش @{name}",
"status.request_quote": "درخواست نقل",
"status.revoke_quote": "حذف فرسته‌ام از فرسته @{name}",
"status.sensitive_warning": "محتوای حساس",
"status.share": "هم‌رسانی",
@@ -922,14 +993,15 @@
"upload_button.label": "افزودن تصاویر، ویدیو یا یک پروندهٔ صوتی",
"upload_error.limit": "از حد مجاز بارگذاری پرونده فراتر رفتید.",
"upload_error.poll": "بارگذاری پرونده در نظرسنجی‌ها مجاز نیست.",
"upload_error.quote": "بارگذاری پرونده در نقل‌ها محاز نیست.",
"upload_form.drag_and_drop.instructions": "برای دریافت پیوست رسانه، space را فشار دهید یا وارد کنید. در حین کشیدن، از کلیدهای جهت دار برای حرکت دادن پیوست رسانه در هر جهت معین استفاده کنید. برای رها کردن ضمیمه رسانه در موقعیت جدید خود، مجدداً space یا enter را فشار دهید، یا برای لغو، escape را فشار دهید.",
"upload_form.drag_and_drop.on_drag_cancel": "کشیدن لغو شد. پیوست رسانه {item} حذف شد.",
"upload_form.drag_and_drop.on_drag_end": "پیوست رسانه {item} حذف شد.",
"upload_form.drag_and_drop.on_drag_over": "پیوست رسانه {item} منتقل شد.",
"upload_form.drag_and_drop.on_drag_start": "پیوست رسانه {item} برداشته شد.",
"upload_form.edit": "ویرایش",
"upload_progress.label": "در حال بارگذاری...",
"upload_progress.processing": "در حال پردازش…",
"upload_progress.label": "بار گذاشتن...",
"upload_progress.processing": "پردازش کردن…",
"username.taken": "این نام کاربری گرفته شده. نام دیگری امتحان کنید",
"video.close": "بستن ویدیو",
"video.download": "بارگیری پرونده",
@@ -946,9 +1018,19 @@
"video.volume_down": "کاهش حجم صدا",
"video.volume_up": "افزایش حجم صدا",
"visibility_modal.button_title": "تنظیم نمایانی",
"visibility_modal.direct_quote_warning.text": "اگر تنظبمات کنونی را ذخیره کنید نقل تعبیه شده تبدیل به پیوند خواهد شد.",
"visibility_modal.direct_quote_warning.title": "نقل‌ها نمی‌توانند در اشاره‌های خصوصی تعبیه شوند",
"visibility_modal.header": "نمایانی و برهم‌کنش",
"visibility_modal.helper.direct_quoting": "اشاره‌های خصوصی روی ماستودون نیم‌توانند به دست دیگران نقل شوند.",
"visibility_modal.helper.privacy_editing": "پس از انتشار فرسته نمی‌توان نمایانی را تغییر داد.",
"visibility_modal.helper.privacy_private_self_quote": "خودنقلی فرسته‌های خصوصی نمی‌تواند عمومی شود.",
"visibility_modal.helper.private_quoting": "فرسته‌ّای فقط پی‌گیران روی ماستودون نمی‌توانند به دست دیگران نقل شوند.",
"visibility_modal.helper.unlisted_quoting": "هنگامی که افراد نقلتان می‌کنند فرسته‌شان هم از خط‌زمانی‌های داغ پنهان خواهد بود.",
"visibility_modal.instructions": "واپایش کسانی که می‌توانند با این فرسته تعامل داشته باشند. می‌تواند با رفتن به <link>ترجیحات > پیش‌گزیده‌ها فرستادن</link> تنظیمات را به همهٔ فرسته‌های آینده نیز اعمال کنید.",
"visibility_modal.privacy_label": "نمایانی",
"visibility_modal.quote_followers": "فقط پی‌گیرندگان",
"visibility_modal.quote_label": "چه‌کسی می‌تواند نقل کند",
"visibility_modal.quote_nobody": "فقط من",
"visibility_modal.quote_public": "هرکسی",
"visibility_modal.save": "ذخیره"
}

View File

@@ -3,7 +3,7 @@
"about.contact": "Yhteydenotto:",
"about.default_locale": "Oletus",
"about.disclaimer": "Mastodon on vapaa avoimen lähdekoodin ohjelmisto ja Mastodon gGmbH:n tavaramerkki.",
"about.domain_blocks.no_reason_available": "Syy ei ole ilmoitettu",
"about.domain_blocks.no_reason_available": "Syy ei ole tiedossa",
"about.domain_blocks.preamble": "Mastodonin avulla voi yleensä tarkastella minkä tahansa fediversumiin kuuluvan palvelimen sisältöä ja olla yhteyksissä eri palvelinten käyttäjien kanssa. Nämä poikkeukset koskevat yksin tätä palvelinta.",
"about.domain_blocks.silenced.explanation": "Et yleensä näe tämän palvelimen profiileja ja sisältöä, jollet erityisesti etsi juuri sitä tai liity siihen seuraamalla.",
"about.domain_blocks.silenced.title": "Rajoitettu",
@@ -194,6 +194,7 @@
"community.column_settings.local_only": "Vain paikalliset",
"community.column_settings.media_only": "Vain media",
"community.column_settings.remote_only": "Vain etätilit",
"compose.error.blank_post": "Julkaisu ei voi olla tyhjä.",
"compose.language.change": "Vaihda kieli",
"compose.language.search": "Hae kieliä…",
"compose.published.body": "Julkaisu lähetetty.",
@@ -246,6 +247,11 @@
"confirmations.missing_alt_text.secondary": "Julkaise silti",
"confirmations.missing_alt_text.title": "Lisätäänkö vaihtoehtoinen teksti?",
"confirmations.mute.confirm": "Mykistä",
"confirmations.private_quote_notify.cancel": "Takaisin muokkaukseen",
"confirmations.private_quote_notify.confirm": "Julkaise",
"confirmations.private_quote_notify.do_not_show_again": "Älä näytä tätä viestiä uudelleen",
"confirmations.private_quote_notify.message": "Lainaamasi käyttäjä ja muut mainitut saavat ilmoituksen ja voivat tarkastella julkaisuasi, vaikka he eivät seuraisi sinua.",
"confirmations.private_quote_notify.title": "Jaetaanko seuraajien ja mainittujen käyttäjien kanssa?",
"confirmations.quiet_post_quote_info.dismiss": "Älä muistuta minua uudelleen",
"confirmations.quiet_post_quote_info.got_it": "Selvä",
"confirmations.quiet_post_quote_info.message": "Kun lainaat vaivihkaa julkisia julkaisuja, oma julkaisusi piilotetaan suosittujen julkaisujen aikajanoilta.",
@@ -758,6 +764,7 @@
"privacy_policy.title": "Tietosuojakäytäntö",
"quote_error.edit": "Lainauksia ei voi lisätä julkaisua muokattaessa.",
"quote_error.poll": "Äänestysten lainaaminen ei ole sallittua.",
"quote_error.private_mentions": "Lainaaminen ei ole sallittua yksityismaininnoissa.",
"quote_error.quote": "Vain yksi lainaus kerrallaan on sallittu.",
"quote_error.unauthorized": "Sinulla ei ole valtuuksia lainata tätä julkaisua.",
"quote_error.upload": "Medialiitteiden lainaaminen ei ole sallittua.",
@@ -911,9 +918,12 @@
"status.pin": "Kiinnitä profiiliin",
"status.quote": "Lainaa",
"status.quote.cancel": "Peruuta lainaus",
"status.quote_error.blocked_account_hint.title": "Tämä julkaisu on piilotettu, koska olet estänyt käyttäjän @{name}.",
"status.quote_error.blocked_domain_hint.title": "Tämä julkaisu on piilotettu, koska olet estänyt verkkotunnuksen {domain}.",
"status.quote_error.filtered": "Piilotettu jonkin asettamasi suodattimen takia",
"status.quote_error.limited_account_hint.action": "Näytä kuitenkin",
"status.quote_error.limited_account_hint.title": "Palvelimen {domain} moderaattorit ovat piilottaneet tämän profiilin.",
"status.quote_error.muted_account_hint.title": "Tämä julkaisu on piilotettu, koska olet mykistänyt käyttäjän @{name}.",
"status.quote_error.not_available": "Julkaisu ei saatavilla",
"status.quote_error.pending_approval": "Julkaisu odottaa",
"status.quote_error.pending_approval_popout.body": "Mastodonissa voit hallita, voiko joku lainata sinua. Tämä julkaisu on vireillä siihen asti, että saamme alkuperäisen tekijän hyväksynnän.",
@@ -1008,6 +1018,8 @@
"video.volume_down": "Vähennä äänenvoimakkuutta",
"video.volume_up": "Lisää äänenvoimakkuutta",
"visibility_modal.button_title": "Aseta näkyvyys",
"visibility_modal.direct_quote_warning.text": "Jos tallennat nykyiset asetukset, upotettu lainaus muunnetaan linkiksi.",
"visibility_modal.direct_quote_warning.title": "Lainauksia ei voi upottaa yksityismainintoihin",
"visibility_modal.header": "Näkyvyys ja vuorovaikutus",
"visibility_modal.helper.direct_quoting": "Muut eivät voi lainata Mastodonissa kirjoitettuja yksityismainintoja.",
"visibility_modal.helper.privacy_editing": "Näkyvyyttä ei voi muuttaa julkaisun jälkeen.",

View File

@@ -173,6 +173,8 @@
"column.edit_list": "Broyt lista",
"column.favourites": "Dámdir postar",
"column.firehose": "Beinleiðis rásir",
"column.firehose_local": "Beinleiðis rás hjá hesum ambætaranum",
"column.firehose_singular": "Beinleiðis rás",
"column.follow_requests": "Umbønir at fylgja",
"column.home": "Heim",
"column.list_members": "Rætta limalista",
@@ -192,6 +194,7 @@
"community.column_settings.local_only": "Einans lokalt",
"community.column_settings.media_only": "Einans miðlar",
"community.column_settings.remote_only": "Einans útifrá",
"compose.error.blank_post": "Postar kunnu ikki vera blankir.",
"compose.language.change": "Skift mál",
"compose.language.search": "Leita eftir málum...",
"compose.published.body": "Postur útgivin.",
@@ -244,6 +247,11 @@
"confirmations.missing_alt_text.secondary": "Posta allíkavæl",
"confirmations.missing_alt_text.title": "Legg alternativan tekst afturat?",
"confirmations.mute.confirm": "Doyv",
"confirmations.private_quote_notify.cancel": "Aftur til rættingar",
"confirmations.private_quote_notify.confirm": "Útgev post",
"confirmations.private_quote_notify.do_not_show_again": "Ikki vísa hesi boðini aftur",
"confirmations.private_quote_notify.message": "Persónurin, sum tú siterar, og onnur, sum eru nevnd, verða kunnaði og kunnu síggja postin hjá tær, sjálvt um tey ikki fylgja tær.",
"confirmations.private_quote_notify.title": "Deil við fylgjarum og nevndum brúkarum?",
"confirmations.quiet_post_quote_info.dismiss": "Ikki minna meg á tað aftur",
"confirmations.quiet_post_quote_info.got_it": "Eg skilji",
"confirmations.quiet_post_quote_info.message": "Tá tú siterar ein stillan almennan post, verður posturin hjá tær fjaldur frá tíðarlinjum, ið vísa vælumtóktar postar.",
@@ -756,6 +764,7 @@
"privacy_policy.title": "Privatlívspolitikkur",
"quote_error.edit": "Sitatir kunnu ikki leggjast afturat tá tú rættar ein post.",
"quote_error.poll": "Tað er ikki loyvt at sitera spurnarkanningar.",
"quote_error.private_mentions": "Sitering er ikki loyvd við beinleiðis umrøðum.",
"quote_error.quote": "Bara ein sitering er loyvd í senn.",
"quote_error.unauthorized": "Tú hevur ikki rættindi at sitera hendan postin.",
"quote_error.upload": "Sitering er ikki loyvd um miðlar eru viðheftir.",
@@ -909,9 +918,12 @@
"status.pin": "Ger fastan í vangan",
"status.quote": "Sitat",
"status.quote.cancel": "Ógilda sitat",
"status.quote_error.blocked_account_hint.title": "Hesin posturin er fjaldur, tí tú hevur blokerað @{name}.",
"status.quote_error.blocked_domain_hint.title": "Hesin posturin er fjaldur, tí tú hevur blokerað @{domain}.",
"status.quote_error.filtered": "Eitt av tínum filtrum fjalir hetta",
"status.quote_error.limited_account_hint.action": "Vís kortini",
"status.quote_error.limited_account_hint.title": "Hendan kontan er fjald av kjakleiðarunum á {domain}.",
"status.quote_error.muted_account_hint.title": "Hesin posturin er fjaldur, tí tú hevur doyvt @{name}.",
"status.quote_error.not_available": "Postur ikki tøkur",
"status.quote_error.pending_approval": "Postur bíðar",
"status.quote_error.pending_approval_popout.body": "Á Mastodon kanst tú avgera, um onkur kann sitera teg. Hesin posturin bíðar eftir góðkenning frá upprunahøvundinum.",
@@ -1006,6 +1018,8 @@
"video.volume_down": "Minka ljóðstyrki",
"video.volume_up": "Øk um ljóðstyrki",
"visibility_modal.button_title": "Set sýni",
"visibility_modal.direct_quote_warning.text": "Um tú goymir verandi stillingar, verður innskotna sitatið umskapað til eitt leinki.",
"visibility_modal.direct_quote_warning.title": "Sitatir kunnu ikki innskjótast í privatar umrøður",
"visibility_modal.header": "Sýni og samvirkni",
"visibility_modal.helper.direct_quoting": "Privatar umrøður, sum eru skrivaðar á Mastodon, kunnu ikki siterast av øðrum.",
"visibility_modal.helper.privacy_editing": "Sýni kann ikki broytast eftir, at ein postur er útgivin.",

View File

@@ -21,29 +21,35 @@
"account.block_domain": "Bloquer le domaine {domain}",
"account.block_short": "Bloquer",
"account.blocked": "Bloqué·e",
"account.blocking": "Bloquer",
"account.blocking": "Bloqué",
"account.cancel_follow_request": "Retirer cette demande d'abonnement",
"account.copy": "Copier le lien vers le profil",
"account.direct": "Mention privée @{name}",
"account.disable_notifications": "Ne plus me notifier quand @{name} publie",
"account.domain_blocking": "Bloquer domaine",
"account.domain_blocking": "Domaine bloqué",
"account.edit_profile": "Modifier le profil",
"account.edit_profile_short": "Modifier",
"account.enable_notifications": "Me notifier quand @{name} publie",
"account.endorse": "Inclure sur profil",
"account.familiar_followers_many": "Suivi par {name1},{name2}, et {othersCount, plural,one {une personne connue} other {# autres personnel connues}}",
"account.familiar_followers_many": "Suivi par {name1}, {name2}, et {othersCount, plural, one {une autre personne que vous suivez} other {# autres personnes que vous suivez}}",
"account.familiar_followers_one": "Suivi par {name1}",
"account.familiar_followers_two": "Suivi par {name1} et {name2}",
"account.featured": "En vedette",
"account.featured.accounts": "Profiles",
"account.featured.accounts": "Profils",
"account.featured.hashtags": "Hashtags",
"account.featured_tags.last_status_at": "Dernière publication {date}",
"account.featured_tags.last_status_never": "Aucune publication",
"account.follow": "Suivre",
"account.follow_back": "Suivre en retour",
"account.follow_back_short": "Suivre en retour",
"account.follow_request": "Demande dabonnement",
"account.follow_request_cancel": "Annuler la demande",
"account.follow_request_cancel_short": "Annuler",
"account.follow_request_short": "Requête",
"account.followers": "abonné·e·s",
"account.followers.empty": "Personne ne suit ce compte pour l'instant.",
"account.followers_counter": "{count, plural, one {{counter} abonné·e} other {{counter} abonné·e·s}}",
"account.followers_you_know_counter": "Vous connaissez {counter}",
"account.followers_you_know_counter": "{count, plural, one {{counter} suivi}, other {{counter} suivis}}",
"account.following": "Abonné·e",
"account.following_counter": "{count, plural, one {{counter} abonnement} other {{counter} abonnements}}",
"account.follows.empty": "Ce compte ne suit personne présentement.",
@@ -68,10 +74,10 @@
"account.open_original_page": "Ouvrir la page d'origine",
"account.posts": "Publications",
"account.posts_with_replies": "Publications et réponses",
"account.remove_from_followers": "Retirer {name} des suiveurs",
"account.remove_from_followers": "Retirer {name} des abonnés",
"account.report": "Signaler @{name}",
"account.requested_follow": "{name} a demandé à vous suivre",
"account.requests_to_follow_you": "Demande a vous suivre",
"account.requests_to_follow_you": "Demande à vous suivre",
"account.share": "Partager le profil de @{name}",
"account.show_reblogs": "Afficher les boosts de @{name}",
"account.statuses_counter": "{count, plural, one {{counter} message} other {{counter} messages}}",
@@ -100,7 +106,7 @@
"alert.unexpected.title": "Oups!",
"alt_text_badge.title": "Texte alternatif",
"alt_text_modal.add_alt_text": "Ajouter un texte alternatif",
"alt_text_modal.add_text_from_image": "Ajouter le texte provenant de l'image",
"alt_text_modal.add_text_from_image": "Extraire le texte de l'image",
"alt_text_modal.cancel": "Annuler",
"alt_text_modal.change_thumbnail": "Changer la vignette",
"alt_text_modal.describe_for_people_with_hearing_impairments": "Décrire pour les personnes ayant des difficultés daudition…",
@@ -167,6 +173,8 @@
"column.edit_list": "Modifier la liste",
"column.favourites": "Favoris",
"column.firehose": "Flux en direct",
"column.firehose_local": "Flux en direct pour ce serveur",
"column.firehose_singular": "Flux en direct",
"column.follow_requests": "Demande d'abonnement",
"column.home": "Accueil",
"column.list_members": "Gérer les membres de la liste",
@@ -186,6 +194,7 @@
"community.column_settings.local_only": "Local seulement",
"community.column_settings.media_only": "Média seulement",
"community.column_settings.remote_only": "À distance seulement",
"compose.error.blank_post": "Le message ne peut être laissé vide.",
"compose.language.change": "Changer de langue",
"compose.language.search": "Rechercher des langues…",
"compose.published.body": "Publiée.",
@@ -238,6 +247,11 @@
"confirmations.missing_alt_text.secondary": "Publier quand-même",
"confirmations.missing_alt_text.title": "Ajouter un texte alternatif?",
"confirmations.mute.confirm": "Masquer",
"confirmations.private_quote_notify.cancel": "Retour à l'édition",
"confirmations.private_quote_notify.confirm": "Publier",
"confirmations.private_quote_notify.do_not_show_again": "Ne plus afficher ce message",
"confirmations.private_quote_notify.message": "La personne citée et celles mentionnées seront notifiées et pourront voir le message, même si elles ne vous suivent pas.",
"confirmations.private_quote_notify.title": "Partager avec les personnes abonnées et mentionnées?",
"confirmations.quiet_post_quote_info.dismiss": "Ne plus me rappeler",
"confirmations.quiet_post_quote_info.got_it": "Compris",
"confirmations.quiet_post_quote_info.message": "Lorsque vous citez un message public silencieux, votre message sera caché des fils tendances.",
@@ -327,6 +341,7 @@
"empty_column.bookmarked_statuses": "Vous n'avez pas de publications parmi vos signets. Lorsque vous en ajouterez une, elle apparaîtra ici.",
"empty_column.community": "Le fil local est vide. Écrivez donc quelque chose pour le remplir!",
"empty_column.direct": "Vous n'avez pas encore de mentions privées. Quand vous en envoyez ou en recevez, elles apparaîtront ici.",
"empty_column.disabled_feed": "Ce flux a été désactivé par les administrateur·rice·s de votre serveur.",
"empty_column.domain_blocks": "Il ny a aucun domaine bloqué pour le moment.",
"empty_column.explore_statuses": "Rien n'est en tendance présentement. Revenez plus tard!",
"empty_column.favourited_statuses": "Vous navez pas encore de publications favorites. Lorsque vous en ajouterez une, elle apparaîtra ici.",
@@ -747,7 +762,9 @@
"privacy.unlisted.short": "Public discret",
"privacy_policy.last_updated": "Dernière mise à jour {date}",
"privacy_policy.title": "Politique de confidentialité",
"quote_error.edit": "Les citations ne peuvent pas être ajoutés lors de l'édition d'un message.",
"quote_error.poll": "Les citations ne sont pas autorisées avec les sondages.",
"quote_error.private_mentions": "La citation n'est pas autorisée avec les mentions privées.",
"quote_error.quote": "Une seule citation à la fois est autorisée.",
"quote_error.unauthorized": "Vous n'êtes pas autorisé⋅e à citer cette publication.",
"quote_error.upload": "La citation n'est pas autorisée avec un média joint.",
@@ -869,6 +886,7 @@
"status.contains_quote": "Contient la citation",
"status.context.loading": "Chargement de réponses supplémentaires",
"status.context.loading_error": "Impossible de charger les nouvelles réponses",
"status.context.loading_success": "Nouvelles réponses ont été chargées",
"status.context.more_replies_found": "Plus de réponses trouvées",
"status.context.retry": "Réessayer",
"status.context.show": "Montrer",
@@ -900,9 +918,12 @@
"status.pin": "Épingler sur profil",
"status.quote": "Citer",
"status.quote.cancel": "Annuler la citation",
"status.quote_error.blocked_account_hint.title": "Ce message est masqué car vous avez bloqué @{name}.",
"status.quote_error.blocked_domain_hint.title": "Ce message est masqué car vous avez bloqué {domain}.",
"status.quote_error.filtered": "Caché en raison de l'un de vos filtres",
"status.quote_error.limited_account_hint.action": "Afficher quand même",
"status.quote_error.limited_account_hint.title": "Ce profil a été masqué par la modération de {domain}.",
"status.quote_error.muted_account_hint.title": "Ce message est masqué car vous avez Mis en sourdine @{name}.",
"status.quote_error.not_available": "Publication non disponible",
"status.quote_error.pending_approval": "Publication en attente",
"status.quote_error.pending_approval_popout.body": "Sur Mastodon, vous pouvez contrôler si quelqu'un peut vous citer. Ce message est en attente pendant que nous recevons l'approbation de l'auteur original.",
@@ -997,6 +1018,8 @@
"video.volume_down": "Baisser le volume",
"video.volume_up": "Augmenter le volume",
"visibility_modal.button_title": "Définir la visibilité",
"visibility_modal.direct_quote_warning.text": "Si vous enregistrez les paramètres actuels, la citation sera convertie en lien.",
"visibility_modal.direct_quote_warning.title": "Les citations ne peuvent pas être intégrées dans les mentions privées",
"visibility_modal.header": "Visibilité et interactions",
"visibility_modal.helper.direct_quoting": "Les mentions privées rédigées sur Mastodon ne peuvent pas être citées par d'autres personnes.",
"visibility_modal.helper.privacy_editing": "La visibilité ne peut pas être modifiée après la publication d'un message.",

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