From ada329cf3cedf74412683a44a4def527ad1aa892 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 15:29:40 +0000 Subject: [PATCH 1/7] fix: remember PDF preview aspect ratio so the feed stops jumping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A PDF card in the feed rendered its first page from scratch on every visit, and until that render landed it collapsed to a bare filename row. Scrolling away and back therefore replayed the same grow-into-place animation the very first sight of the post has to accept, because — unlike images and videos — nothing recorded the page's shape. Store it in MediaAspectRatioCache, the same URL-keyed cache the image and video paths already fill, and reserve that box in the loading placeholder. The first sight of a PDF still grows into place, since that render is what fills the cache; every later visit lays out at the right shape immediately. The cache is read ahead of the imeta `dim` tag here: it holds the page size this card measured itself, so an author-supplied `dim` that disagreed would guarantee the jump on every single visit. The 0.2 ratio floor moved into a shared previewAspectRatio() helper so the reserved box and the rendered thumbnail are clamped identically — applying it on one side only would misreserve for exactly the tall pages the clamp exists to protect. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019DjHVxiXFncNhoKuNgtSio --- .../ui/components/pdf/PdfPreviewCard.kt | 67 +++++++++++++++++-- .../pdf/PdfPreviewAspectRatioTest.kt | 58 ++++++++++++++++ 2 files changed, 119 insertions(+), 6 deletions(-) create mode 100644 amethyst/src/test/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewAspectRatioTest.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt index 0b975ede46..14d4735e29 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt @@ -26,6 +26,7 @@ import android.os.ParcelFileDescriptor import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio @@ -36,6 +37,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.graphics.asImageBitmap @@ -44,11 +46,15 @@ import androidx.compose.ui.platform.LocalWindowInfo import androidx.core.graphics.createBitmap import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.richtext.MediaUrlPdf +import com.vitorpamplona.amethyst.commons.ui.components.LoadingAnimation +import com.vitorpamplona.amethyst.model.MediaAspectRatioCache import com.vitorpamplona.amethyst.ui.components.ClickableUrl import com.vitorpamplona.amethyst.ui.components.FileAttachmentRow import com.vitorpamplona.amethyst.ui.components.ShareMediaAction import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer +import com.vitorpamplona.amethyst.ui.theme.Size40dp +import com.vitorpamplona.amethyst.ui.theme.Size6dp import com.vitorpamplona.amethyst.ui.theme.innerPostModifier import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CancellationException @@ -58,11 +64,26 @@ import kotlinx.coroutines.withContext // Hard ceiling on the inline thumbnail bitmap, in pixels. Prevents OOM on very tall/large pages. private const val THUMBNAIL_MAX_DIM_PX = 1600 +// Floor for the width/height ratio the card lays out with. A pathologically tall page (a receipt, +// a single-column banner) would otherwise reserve a screenful of height for a sliver of content. +private const val MIN_PREVIEW_ASPECT_RATIO = 0.2f + +/** + * The ratio the card actually lays out with, given a page's natural width/height. The placeholder + * and the loaded thumbnail both go through here so the box reserved while loading is the exact box + * the rendered page lands in — the clamp has to be applied on both sides or the reservation is + * wrong for the very pages it exists to protect. + */ +internal fun previewAspectRatio(pageAspectRatio: Float): Float = pageAspectRatio.coerceAtLeast(MIN_PREVIEW_ASPECT_RATIO) + data class PdfPreview( val thumbnail: Bitmap, val pageCount: Int, - val aspectRatio: Float, -) + val pageWidth: Int, + val pageHeight: Int, +) { + val aspectRatio: Float = pageWidth.toFloat() / pageHeight.toFloat() +} private sealed class PdfLoadState { data object Loading : PdfLoadState() @@ -104,6 +125,14 @@ private fun LoadedPdfPreviewCard( containerWidthPx.coerceAtMost(THUMBNAIL_MAX_DIM_PX).coerceAtLeast(1) } + // Read in composition — never inside a remember — so this recomposes the moment the render + // below fills the entry in. On a revisit the entry is already there and the placeholder can + // reserve the right box from the first frame, which is the whole point of caching it. + // The cache is consulted ahead of the imeta `dim` because it holds the page size this card + // measured itself: an author-supplied `dim` that disagrees would guarantee the jump on every + // single visit, which is exactly what this is here to stop. + val knownAspectRatio = MediaAspectRatioCache.get(content.url) ?: content.dim?.aspectRatio() + @Suppress("ProduceStateDoesNotAssignValue") val state by produceState(initialValue = PdfLoadState.Loading, key1 = content.url, key2 = targetWidthPx) { value = @@ -115,6 +144,13 @@ private fun LoadedPdfPreviewCard( withContext(Dispatchers.IO) { renderFirstPage(snapshot.data.toFile(), targetWidthPx) } + }.also { result -> + // Same cache the image and video paths use, so a PDF that has been rendered + // once lays out at its real shape on every later visit instead of growing + // from a bare filename row into a full page. + if (result is PdfLoadState.Ready) { + MediaAspectRatioCache.add(content.url, result.preview.pageWidth, result.preview.pageHeight) + } } } catch (e: Exception) { if (e is CancellationException) throw e @@ -134,7 +170,7 @@ private fun LoadedPdfPreviewCard( when (val current = state) { is PdfLoadState.Loading -> { - PdfSkeletonCard(filename) + PdfSkeletonCard(filename, knownAspectRatio) } is PdfLoadState.Failed -> { @@ -158,7 +194,7 @@ private fun LoadedPdfPreviewCard( modifier = Modifier .fillMaxWidth() - .aspectRatio(current.preview.aspectRatio.coerceAtLeast(0.2f)), + .aspectRatio(previewAspectRatio(current.preview.aspectRatio)), ) FilenameRow(filename = filename, subtitle = pageCountLabel(current.preview.pageCount)) @@ -187,8 +223,26 @@ private fun PlaceholderPdfCard( } @Composable -private fun PdfSkeletonCard(filename: String) { +private fun PdfSkeletonCard( + filename: String, + aspectRatio: Float?, +) { Column(modifier = MaterialTheme.colorScheme.innerPostModifier.fillMaxWidth()) { + // Only known on a revisit — the first render is what fills the cache — so the first sight of + // a PDF still grows into place. From then on the page's box is held open while it renders + // and the feed stays put. + if (aspectRatio != null) { + Box( + modifier = + Modifier + .fillMaxWidth() + .aspectRatio(previewAspectRatio(aspectRatio)), + contentAlignment = Alignment.Center, + ) { + LoadingAnimation(Size40dp, Size6dp) + } + } + FilenameRow(filename = filename, subtitle = "Loading…") Spacer(modifier = DoubleVertSpacer) } @@ -223,7 +277,8 @@ private fun renderFirstPage( PdfPreview( thumbnail = bitmap, pageCount = pageCount, - aspectRatio = page.width.toFloat() / page.height.toFloat(), + pageWidth = page.width, + pageHeight = page.height, ), ) } diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewAspectRatioTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewAspectRatioTest.kt new file mode 100644 index 0000000000..146bb46e4e --- /dev/null +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewAspectRatioTest.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.components.pdf + +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * The placeholder shown while a previously-seen PDF re-renders reserves its box from the ratio + * cached in `MediaAspectRatioCache`, and the loaded thumbnail sizes itself from the ratio the + * renderer reports. Those are the same number, so both sides must clamp it identically — a + * placeholder that skipped the clamp would reserve a different box than the page lands in, which is + * the jump it exists to remove. + */ +class PdfPreviewAspectRatioTest { + @Test + fun ordinaryPortraitPageIsUnchanged() { + // US Letter, in PostScript points. + assertEquals(612f / 792f, previewAspectRatio(612f / 792f), 0.0001f) + } + + @Test + fun landscapePageIsUnchanged() { + assertEquals(792f / 612f, previewAspectRatio(792f / 612f), 0.0001f) + } + + @Test + fun pathologicallyTallPageIsClamped() { + // A 200x4000 receipt would otherwise reserve 20 screens of height for a sliver of content. + assertEquals(0.2f, previewAspectRatio(200f / 4000f), 0.0001f) + } + + @Test + fun theClampIsTheSameNumberBothSidesUse() { + val cachedOnRevisit = 200f / 4000f + val reportedByTheRenderer = 200f / 4000f + + assertEquals(previewAspectRatio(reportedByTheRenderer), previewAspectRatio(cachedOnRevisit), 0.0f) + } +} From 130c69377a50c37c5c9a419a3fa8d85af01e8918 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 15:49:21 +0000 Subject: [PATCH 2/7] fix: guard PDF preview against degenerate page sizes and main-thread cache I/O MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three defects found auditing the aspect-ratio caching work. Modifier.aspectRatio throws IllegalArgumentException on 0f and on NaN, and the 0.2 clamp catches neither — NaN.coerceAtLeast(x) is NaN, since every comparison against NaN is false. Both values are reachable: a malformed PDF whose first page measures 0x0, and an imeta `dim` that reaches 0x0 by truncation ("0.4x0.4"), which DimensionTag.parse does not reject because it only rejects the literal string "0x0". Reading `dim` here is new, so this turned a tag any relay can carry into a crash of the whole feed's composition. previewAspectRatio() is now the single place that guarantees the number is finite and positive, falling back to Letter portrait. PdfFetcher.fetchSnapshot ran its cache-hit fast path before entering Dispatchers.IO, so openSnapshot() executed on whatever thread the caller was on — for the feed card, the main thread. That call contends on the global DiskLruCache lock, which Coil's cleanup pass holds across a burst of unlink syscalls (documented at length in DeferredDeleteFileSystem), so a frame could block for the whole burst. The hit path is exactly the one a PDF card takes when it scrolls back into view. The viewer dialog already wrapped its call; now the fetcher guarantees it for every caller. Image(bitmap = …) holds its BitmapPainter in a remember keyed on the ImageBitmap, but asImageBitmap() allocates a fresh identity-compared wrapper per call, so calling it inline rebuilt the painter on every recomposition. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019DjHVxiXFncNhoKuNgtSio --- .../amethyst/ui/components/pdf/PdfFetcher.kt | 14 ++++--- .../ui/components/pdf/PdfPreviewCard.kt | 26 +++++++++++- .../pdf/PdfPreviewAspectRatioTest.kt | 42 +++++++++++++++++++ 3 files changed, 75 insertions(+), 7 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfFetcher.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfFetcher.kt index 4932b0a97f..c1fcee999a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfFetcher.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfFetcher.kt @@ -41,11 +41,16 @@ object PdfFetcher { suspend fun fetchSnapshot( url: String, okHttpClient: (String) -> OkHttpClient, - ): DiskCache.Snapshot { - val diskCache = Amethyst.instance.diskCache - diskCache.openSnapshot(url)?.let { return it } + ): DiskCache.Snapshot = + withContext(Dispatchers.IO) { + val diskCache = Amethyst.instance.diskCache + // Covers the cache-hit fast path too, not just the download below it. openSnapshot() + // contends on the global DiskLruCache lock, which Coil's cleanup pass holds across a + // burst of unlink syscalls (see DeferredDeleteFileSystem) — calling it from a caller + // that happens to be on the main thread stalls the frame for that whole burst, and the + // hit path is exactly the one a feed takes when a PDF card scrolls back into view. + diskCache.openSnapshot(url)?.let { return@withContext it } - return withContext(Dispatchers.IO) { val editor = diskCache.openEditor(url) ?: throw IOException("Unable to open cache editor for $url") try { val request = @@ -71,5 +76,4 @@ object PdfFetcher { throw t } } - } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt index 14d4735e29..2c931fc0bc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt @@ -68,13 +68,30 @@ private const val THUMBNAIL_MAX_DIM_PX = 1600 // a single-column banner) would otherwise reserve a screenful of height for a sliver of content. private const val MIN_PREVIEW_ASPECT_RATIO = 0.2f +// Shape to assume when the page reports no usable size at all. US Letter portrait — the +// overwhelmingly common page shape, and the least surprising box to hold open for an unknown one. +private const val DEFAULT_PAGE_ASPECT_RATIO = 612f / 792f + /** * The ratio the card actually lays out with, given a page's natural width/height. The placeholder * and the loaded thumbnail both go through here so the box reserved while loading is the exact box * the rendered page lands in — the clamp has to be applied on both sides or the reservation is * wrong for the very pages it exists to protect. + * + * This is also the one place that guarantees `Modifier.aspectRatio` is handed a finite, positive + * number. It throws `IllegalArgumentException` on `0f` and on `NaN`, and the clamp below does not + * catch either: `NaN.coerceAtLeast(x)` is `NaN`, because every comparison against `NaN` is false. + * Both are reachable from real input — a malformed PDF whose first page measures 0x0, or an imeta + * `dim` that survives DimensionTag.parse's only-rejects-literal-"0x0" check as 0x0 anyway + * (`"0.4x0.4"` truncates to it). Without this guard either one takes down the whole feed's + * composition, from a tag any relay can carry. */ -internal fun previewAspectRatio(pageAspectRatio: Float): Float = pageAspectRatio.coerceAtLeast(MIN_PREVIEW_ASPECT_RATIO) +internal fun previewAspectRatio(pageAspectRatio: Float): Float = + if (!pageAspectRatio.isFinite() || pageAspectRatio <= 0f) { + DEFAULT_PAGE_ASPECT_RATIO + } else { + pageAspectRatio.coerceAtLeast(MIN_PREVIEW_ASPECT_RATIO) + } data class PdfPreview( val thumbnail: Bitmap, @@ -186,8 +203,13 @@ private fun LoadedPdfPreviewCard( onLongClick = { sharePopupExpanded.value = true }, ), ) { + // asImageBitmap() allocates a fresh wrapper on every call and the wrapper compares + // by identity, so calling it inline would defeat the remember(bitmap) that Image + // uses to hold its BitmapPainter — every recomposition would rebuild the painter. + val thumbnail = remember(current.preview.thumbnail) { current.preview.thumbnail.asImageBitmap() } + Image( - bitmap = current.preview.thumbnail.asImageBitmap(), + bitmap = thumbnail, contentDescription = content.description ?: filename, contentScale = ContentScale.FillWidth, filterQuality = FilterQuality.High, diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewAspectRatioTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewAspectRatioTest.kt index 146bb46e4e..5750780d29 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewAspectRatioTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewAspectRatioTest.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.ui.components.pdf import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue import org.junit.Test /** @@ -55,4 +56,45 @@ class PdfPreviewAspectRatioTest { assertEquals(previewAspectRatio(reportedByTheRenderer), previewAspectRatio(cachedOnRevisit), 0.0f) } + + /** + * `Modifier.aspectRatio` requires a finite ratio greater than zero and throws otherwise, so + * every one of these would crash the composition of the whole feed if it reached the modifier. + * A page measuring 0x0 and an imeta `dim` of `"0.4x0.4"` (which truncates to 0x0, slipping past + * the parser's literal-`"0x0"` rejection) both arrive here as 0/0. + */ + @Test + fun degenerateSizesNeverReachTheModifier() { + assertUsable(previewAspectRatio(ratioOf(0, 0))) + assertUsable(previewAspectRatio(ratioOf(100, 0))) + assertUsable(previewAspectRatio(ratioOf(0, 100))) + assertUsable(previewAspectRatio(-1f)) + } + + /** + * Pins the reason the guard above cannot just be the clamp: every comparison against NaN is + * false, so `coerceAtLeast` returns NaN unchanged rather than lifting it to the floor. + */ + @Test + fun clampingAloneDoesNotCatchNaN() { + assertTrue(ratioOf(0, 0).coerceAtLeast(0.2f).isNaN()) + } + + @Test + fun unknownSizedPagesFallBackToPortrait() { + // Taller than wide, so an unknown page reserves a portrait box rather than a squat one. + assertTrue(previewAspectRatio(ratioOf(0, 0)) < 1f) + } + + // Computed rather than written as a literal so the compiler doesn't fold it into a + // division-by-zero warning — these degenerate sizes are the point of the test. + private fun ratioOf( + width: Int, + height: Int, + ): Float = width.toFloat() / height.toFloat() + + private fun assertUsable(ratio: Float) { + assertTrue("$ratio is not finite", ratio.isFinite()) + assertTrue("$ratio is not > 0", ratio > 0f) + } } From b070d1ed5c042315b5f23ba40a59ea00fb25a219 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 15:51:16 +0000 Subject: [PATCH 3/7] fix: skip zero-sized imeta dim when reserving the PDF preview box DimensionTag.parse only rejects the literal string "0x0", so a `dim` of "0.4x0.4" truncates past it into DimensionTag(0, 0). Guard with hasSize() before reading aspectRatio(), matching what ImageGallery already does, so a PDF with such a tag reserves nothing rather than a box computed from 0/0. previewAspectRatio() still backstops the rendered-page case. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019DjHVxiXFncNhoKuNgtSio --- .../amethyst/ui/components/pdf/PdfPreviewCard.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt index 2c931fc0bc..739e81935b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt @@ -148,7 +148,10 @@ private fun LoadedPdfPreviewCard( // The cache is consulted ahead of the imeta `dim` because it holds the page size this card // measured itself: an author-supplied `dim` that disagrees would guarantee the jump on every // single visit, which is exactly what this is here to stop. - val knownAspectRatio = MediaAspectRatioCache.get(content.url) ?: content.dim?.aspectRatio() + // hasSize() before aspectRatio(), the same guard ImageGallery applies: DimensionTag.parse only + // rejects the literal "0x0", so a `dim` like "0.4x0.4" truncates to 0x0 and would otherwise + // reserve a box off a 0/0 ratio. Reserving nothing is the honest answer for a size we don't have. + val knownAspectRatio = MediaAspectRatioCache.get(content.url) ?: content.dim?.takeIf { it.hasSize() }?.aspectRatio() @Suppress("ProduceStateDoesNotAssignValue") val state by produceState(initialValue = PdfLoadState.Loading, key1 = content.url, key2 = targetWidthPx) { From 09fdf3d0b27bdb7f1d7fc3eef6f407538fc78430 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 16:16:59 +0000 Subject: [PATCH 4/7] fix: never hand a zero or NaN aspect ratio to a layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DimensionTag.parse rejects only the literal string "0x0", so an imeta `dim` of "0.4x0.4" truncates past it into DimensionTag(0, 0). aspectRatio() then returns 0/0 — NaN — and Modifier.aspectRatio throws IllegalArgumentException on NaN and on 0f alike, taking down the composition around the media rather than mis-sizing it. Eleven call sites fed dim?.aspectRatio() straight into a layout, so a tag any relay can carry could crash a feed containing an image or a video. ImageGallery was the one site already guarding with hasSize(). Closed at both sources rather than at the eleven consumers: - DimensionTag gains aspectRatioOrNull(), null unless hasSize(), and every production call site now uses it. The raw accessor stays for callers that want the number, with KDoc pointing at the safe one. - MediaAspectRatioCache.add checked only the divisor, so a reported width of 0 stored a perfectly dividable 0f — just as unusable downstream. It now requires both sides, leaving the entry empty for a later, better report. mediaSizingModifier, where six of the eleven converge, treats a non-finite or non-positive ratio as no ratio — a backstop for a future caller that forgets, not the primary guard. Every remaining ratio reaching Modifier.aspectRatio was traced: VideoView already guarded its own division, PiP and SoftwareApp read only the cache, and the rest are literals. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019DjHVxiXFncNhoKuNgtSio --- .../amethyst/model/MediaAspectRatioCache.kt | 6 ++- .../amethyst/ui/components/GifVideoView.kt | 4 +- .../amethyst/ui/components/ImageGallery.kt | 6 +-- .../ui/components/SensitivityWarning.kt | 12 +++++- .../ui/components/ZoomableContentDialog.kt | 4 +- .../ui/components/ZoomableContentView.kt | 8 ++-- .../ui/components/pdf/PdfPreviewCard.kt | 5 +-- .../amethyst/ui/note/types/PictureDisplay.kt | 2 +- .../amethyst/ui/note/types/VideoDisplay.kt | 2 +- .../loggedIn/pictures/PictureCardCompose.kt | 2 +- .../loggedIn/shorts/VideoCardCompose.kt | 2 +- .../loggedIn/video/FileHeaderCardCompose.kt | 2 +- .../nip94FileMetadata/tags/DimensionTag.kt | 15 ++++++++ .../tags/DimensionTagTest.kt | 37 +++++++++++++++++++ 14 files changed, 83 insertions(+), 24 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MediaAspectRatioCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MediaAspectRatioCache.kt index c5b9acfee8..5057f55847 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MediaAspectRatioCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MediaAspectRatioCache.kt @@ -56,12 +56,16 @@ object MediaAspectRatioCache : MutableMediaAspectRatioCache { override fun get(url: String): Float? = entry(url).value + // Both sides are checked, not just the divisor: a zero width divides cleanly to 0f, and 0f is + // just as unusable downstream as a division by zero — `Modifier.aspectRatio` throws on it. A + // reported size that cannot be laid out is stored as no size at all, leaving the entry empty + // for a later, better report to fill. override fun add( url: String, width: Int, height: Int, ) { - if (height > 1) { + if (width > 0 && height > 1) { entry(url).value = width.toFloat() / height.toFloat() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/GifVideoView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/GifVideoView.kt index e251521024..c93211bafa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/GifVideoView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/GifVideoView.kt @@ -66,11 +66,11 @@ fun GifVideoView( accountViewModel: AccountViewModel, thumbhash: String? = null, ) { - // Pure read path — DimensionTag.aspectRatio() is a one-line int division and + // Pure read path — DimensionTag.aspectRatioOrNull() is a one-line int division and // MediaAspectRatioCache.get() is a synchronized LruCache lookup. Wrapping this in // remember() to avoid the recompute would cost more (slot read + N equality checks) // than the work it saves; that's why this stays as a plain expression. - val ratio = dimensions?.aspectRatio() ?: MediaAspectRatioCache.get(videoUri) + val ratio = dimensions?.aspectRatioOrNull() ?: MediaAspectRatioCache.get(videoUri) val autoPlay = accountViewModel.settings.autoPlayVideos() val borderModifier = if (roundedCorner) MaterialTheme.colorScheme.imageModifier else Modifier val context = LocalContext.current diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ImageGallery.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ImageGallery.kt index 450125d2a7..7e0290a40d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ImageGallery.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ImageGallery.kt @@ -62,11 +62,7 @@ private data class FirstImageOrientation( val isLandscape: Boolean, ) -private fun MediaUrlImage.resolvedAspectRatio(): Float? = - dim - ?.takeIf { it.hasSize() } - ?.aspectRatio() - ?: MediaAspectRatioCache.get(url) +private fun MediaUrlImage.resolvedAspectRatio(): Float? = dim?.aspectRatioOrNull() ?: MediaAspectRatioCache.get(url) private fun MediaUrlImage?.resolveOrientation( landscapeDefaultAspectRatio: Float, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt index fd8d7b7e29..6aff101a1d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt @@ -240,13 +240,23 @@ fun BlurhashGridBackdrop(media: List) { } } +/** + * Sizes a media container from a reported width/height ratio, falling back to width-only when + * there is no usable one. + * + * A ratio that is not finite and positive counts as no ratio: `Modifier.aspectRatio` throws + * `IllegalArgumentException` on `0f` and on `NaN`, which would take down the composition around + * the media rather than just mis-sizing it. Callers are expected to hand over a clean value + * (`DimensionTag.aspectRatioOrNull`, `MediaAspectRatioCache`), so this is the backstop for the one + * that forgets, not the primary guard. + */ fun mediaSizingModifier( ratio: Float?, contentScale: ContentScale, ): Modifier = when { contentScale == ContentScale.Crop -> Modifier.fillMaxSize() - ratio != null -> Modifier.fillMaxWidth().aspectRatio(ratio) + ratio != null && ratio > 0f && ratio.isFinite() -> Modifier.fillMaxWidth().aspectRatio(ratio) else -> Modifier.fillMaxWidth() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt index f211bbfa4f..4239c9be68 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt @@ -628,7 +628,7 @@ private fun RenderImageOrVideo( Modifier.fillMaxWidth() } - val ratio = content.dim?.aspectRatio() ?: MediaAspectRatioCache.get(content.url) + val ratio = content.dim?.aspectRatioOrNull() ?: MediaAspectRatioCache.get(content.url) val useLocalBlossomBridge by accountViewModel.useLocalBlossomBridge.collectAsStateWithLifecycle() val bridgedUrl = remember(content.url, useLocalBlossomBridge) { @@ -694,7 +694,7 @@ private fun RenderImageOrVideo( } content.localFile?.let { - val ratio = content.dim?.aspectRatio() ?: MediaAspectRatioCache.get(it.toUri().toString()) + val ratio = content.dim?.aspectRatioOrNull() ?: MediaAspectRatioCache.get(it.toUri().toString()) val modifier = if (ratio != null) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt index 1122b73a57..0928ad0c5c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt @@ -181,7 +181,7 @@ fun ZoomableContentView( when (content) { is MediaUrlImage -> { - val ratio = content.dim?.aspectRatio() ?: MediaAspectRatioCache.get(content.url) + val ratio = content.dim?.aspectRatioOrNull() ?: MediaAspectRatioCache.get(content.url) val bridgedUrl = remember(content.url, useLocalBlossomBridge) { content.toCoilModel(useLocalBlossomBridge) @@ -228,7 +228,7 @@ fun ZoomableContentView( unknownMediaAspectRatio(content.mimeType, content.url) } val ratio = - content.dim?.aspectRatio() + content.dim?.aspectRatioOrNull() ?: MediaAspectRatioCache.get(content.url) ?: fallbackRatio val bridgedUrl = @@ -386,7 +386,7 @@ fun LocalImageView( ) } - val ratio = remember(content) { content.dim?.aspectRatio() ?: MediaAspectRatioCache.get(content.localFile.toString()) } + val ratio = remember(content) { content.dim?.aspectRatioOrNull() ?: MediaAspectRatioCache.get(content.localFile.toString()) } val context = LocalContext.current val imageModel = if (fullResolution) { @@ -510,7 +510,7 @@ fun UrlImageView( alwayShowImage: Boolean = false, fullResolution: Boolean = false, ) { - val ratio = content.dim?.aspectRatio() ?: MediaAspectRatioCache.get(content.url) + val ratio = content.dim?.aspectRatioOrNull() ?: MediaAspectRatioCache.get(content.url) val showImage = remember { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt index 739e81935b..fd3b91b9ab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/pdf/PdfPreviewCard.kt @@ -148,10 +148,7 @@ private fun LoadedPdfPreviewCard( // The cache is consulted ahead of the imeta `dim` because it holds the page size this card // measured itself: an author-supplied `dim` that disagrees would guarantee the jump on every // single visit, which is exactly what this is here to stop. - // hasSize() before aspectRatio(), the same guard ImageGallery applies: DimensionTag.parse only - // rejects the literal "0x0", so a `dim` like "0.4x0.4" truncates to 0x0 and would otherwise - // reserve a box off a 0/0 ratio. Reserving nothing is the honest answer for a size we don't have. - val knownAspectRatio = MediaAspectRatioCache.get(content.url) ?: content.dim?.takeIf { it.hasSize() }?.aspectRatio() + val knownAspectRatio = MediaAspectRatioCache.get(content.url) ?: content.dim?.aspectRatioOrNull() @Suppress("ProduceStateDoesNotAssignValue") val state by produceState(initialValue = PdfLoadState.Loading, key1 = content.url, key2 = targetWidthPx) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PictureDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PictureDisplay.kt index f5fc594951..6143af7dd4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PictureDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PictureDisplay.kt @@ -107,7 +107,7 @@ fun PictureDisplay( } if (images.size == 1) { - val ratio = first.dim?.aspectRatio() ?: MediaAspectRatioCache.get(first.url) + val ratio = first.dim?.aspectRatioOrNull() ?: MediaAspectRatioCache.get(first.url) ContentWarningGate( isSensitive = isSensitive, reasons = reasons, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VideoDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VideoDisplay.kt index 4f5c661810..afe66a4224 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VideoDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VideoDisplay.kt @@ -92,7 +92,7 @@ fun JustVideoDisplay( ) } - val ratio = imeta.dimension?.aspectRatio() ?: MediaAspectRatioCache.get(imeta.url) + val ratio = imeta.dimension?.aspectRatioOrNull() ?: MediaAspectRatioCache.get(imeta.url) ContentWarningGate( isSensitive = isSensitive, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardCompose.kt index 361515edd1..3ca8de4921 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/PictureCardCompose.kt @@ -125,7 +125,7 @@ private fun PictureCardImage( if (images.size == 1) { val single = images.first() - val ratio = single.dim?.aspectRatio() ?: MediaAspectRatioCache.get(single.url) + val ratio = single.dim?.aspectRatioOrNull() ?: MediaAspectRatioCache.get(single.url) ContentWarningGate( isSensitive = isSensitive, reasons = reasons, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/VideoCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/VideoCardCompose.kt index 9e52ec0362..1c54ac585d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/VideoCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/VideoCardCompose.kt @@ -141,7 +141,7 @@ private fun VideoCardImage( ) } - val ratio = imeta.dimension?.aspectRatio() ?: MediaAspectRatioCache.get(imeta.url) + val ratio = imeta.dimension?.aspectRatioOrNull() ?: MediaAspectRatioCache.get(imeta.url) ContentWarningGate( isSensitive = isSensitive, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/FileHeaderCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/FileHeaderCardCompose.kt index d5b18976f2..112c9af02e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/FileHeaderCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/FileHeaderCardCompose.kt @@ -125,7 +125,7 @@ private fun FileHeaderCardImage( } val isImage = content is MediaUrlImage - val ratio = dimensions?.aspectRatio() ?: MediaAspectRatioCache.get(fullUrl) + val ratio = dimensions?.aspectRatioOrNull() ?: MediaAspectRatioCache.get(fullUrl) ContentWarningGate( isSensitive = isSensitive, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/tags/DimensionTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/tags/DimensionTag.kt index 0ec356b6d3..f80391037c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/tags/DimensionTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/tags/DimensionTag.kt @@ -31,8 +31,23 @@ class DimensionTag( val width: Int, val height: Int, ) { + /** + * The raw width/height ratio, which is only meaningful when [hasSize] is true. Anywhere the + * result reaches a layout, use [aspectRatioOrNull] instead. + */ fun aspectRatio() = width.toFloat() / height.toFloat() + /** + * The ratio to lay out with, or null when the tag carries no usable size. + * + * [parse] only rejects the literal string `"0x0"`, so an author-supplied `"0.4x0.4"` truncates + * to 0x0 and slips through — and [aspectRatio] then returns `0/0`, which is `NaN`. Compose's + * `Modifier.aspectRatio` throws `IllegalArgumentException` on `NaN` and on `0f`, and clamping + * does not rescue either: every comparison against `NaN` is false, so `coerceAtLeast` returns + * it unchanged. A tag any relay can carry would otherwise crash the composition around it. + */ + fun aspectRatioOrNull(): Float? = if (hasSize()) aspectRatio() else null + fun hasSize() = width > 0 && height > 0 override fun toString() = "${width}x$height" diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/tags/DimensionTagTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/tags/DimensionTagTest.kt index e97ecbf833..784eeca2c8 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/tags/DimensionTagTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/tags/DimensionTagTest.kt @@ -24,6 +24,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull +import kotlin.test.assertTrue class DimensionTagTest { @Test @@ -72,4 +73,40 @@ class DimensionTagTest { assertNotNull(tag) assertEquals(317f / 498f, tag.aspectRatio()) } + + /** + * [rejectsZeroByZero] passes by comparing the string, so the float path of the Primal + * tolerance above reaches the same 0x0 value by truncation and is kept. + */ + @Test + fun zeroSizedFloatsSlipPastTheZeroByZeroRejection() { + val tag = DimensionTag.parse("0.4x0.4") + assertNotNull(tag) + assertEquals(0, tag.width) + assertEquals(0, tag.height) + } + + /** + * Why [DimensionTag.aspectRatioOrNull] exists: `Modifier.aspectRatio` throws on both of these, + * so a tag that reaches a layout through the raw accessor crashes the composition around it. + */ + @Test + fun rawAspectRatioOfAZeroSizedTagIsNotLayoutSafe() { + assertTrue(DimensionTag(0, 0).aspectRatio().isNaN()) + assertEquals(0f, DimensionTag(0, 100).aspectRatio()) + } + + @Test + fun aspectRatioOrNullRefusesEverySizeALayoutCannotUse() { + assertNull(DimensionTag.parse("0.4x0.4")?.aspectRatioOrNull()) + assertNull(DimensionTag(0, 0).aspectRatioOrNull()) + assertNull(DimensionTag(100, 0).aspectRatioOrNull()) + assertNull(DimensionTag(0, 100).aspectRatioOrNull()) + assertNull(DimensionTag(-1, 10).aspectRatioOrNull()) + } + + @Test + fun aspectRatioOrNullKeepsUsableSizes() { + assertEquals(317f / 498f, DimensionTag(317, 498).aspectRatioOrNull()) + } } From 36e03cce507e43430d841e8f1193406ffbe7c50c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 17:42:19 +0000 Subject: [PATCH 5/7] feat: keep the declared shape of fractional imeta dims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A dim below 1 on either axis truncates to 0 whole pixels, taking its shape with it: "0.75x1" is a legitimate 3:4 and "0.4x0.4" a legitimate square, but both parsed to sizes nothing could lay out. Rejecting them loses information the author actually sent, so keep it instead. DimensionTag now carries the ratio exactly as declared, computed before the truncation to whole pixels, and aspectRatioOrNull() prefers it over the pixel counts. They differ only for fractional dims, where the declared one is the more faithful of the two — "317.9x498.4" is now that ratio rather than 317/498. For whole numbers, every well-formed tag, it is the same number, so nothing about the common path changes. The field is @Transient: it is derived from the tag text, so an instance rebuilt from serialized width and height falls back to the pixel counts rather than carrying a stale ratio. Being transient also keeps it out of the serialized form of every model that embeds a dim (PictureMeta, VideoMeta, ProductImageMeta, …), so this reads and writes old data unchanged. A dim that declares no shape at all — "0x5", "-3x4" — still returns null, since Modifier.aspectRatio throws on everything it would otherwise produce. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019DjHVxiXFncNhoKuNgtSio --- .../nip94FileMetadata/tags/DimensionTag.kt | 51 +++++++++++++---- .../tags/DimensionTagTest.kt | 56 +++++++++++++++++-- 2 files changed, 91 insertions(+), 16 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/tags/DimensionTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/tags/DimensionTag.kt index f80391037c..d502939609 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/tags/DimensionTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/tags/DimensionTag.kt @@ -24,29 +24,43 @@ import androidx.compose.runtime.Stable import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.utils.ensure import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient @Serializable @Stable class DimensionTag( val width: Int, val height: Int, + /** + * The ratio exactly as the author declared it, kept because [width] and [height] are whole + * pixels and truncating to them can lose it — entirely, for a dim below `1` on either axis. + * Only [parse] supplies it, and it is deliberately [Transient]: it is derived from the tag + * text, so a deserialized instance simply falls back to the pixel counts. + */ + @Transient private val declaredRatio: Float? = null, ) { /** - * The raw width/height ratio, which is only meaningful when [hasSize] is true. Anywhere the - * result reaches a layout, use [aspectRatioOrNull] instead. + * The raw width/height ratio in whole pixels, which is only meaningful when [hasSize] is true. + * Anywhere the result reaches a layout, use [aspectRatioOrNull] instead. */ fun aspectRatio() = width.toFloat() / height.toFloat() /** - * The ratio to lay out with, or null when the tag carries no usable size. + * The ratio to lay out with, or null when the tag carries no usable shape. * - * [parse] only rejects the literal string `"0x0"`, so an author-supplied `"0.4x0.4"` truncates - * to 0x0 and slips through — and [aspectRatio] then returns `0/0`, which is `NaN`. Compose's - * `Modifier.aspectRatio` throws `IllegalArgumentException` on `NaN` and on `0f`, and clamping - * does not rescue either: every comparison against `NaN` is false, so `coerceAtLeast` returns - * it unchanged. A tag any relay can carry would otherwise crash the composition around it. + * Prefers the declared ratio over the truncated pixel counts, so a fractional dim keeps the + * shape its author meant: `"0.75x1"` is a legitimate 3:4 even though it rounds down to `0x1`, + * and `"317.9x498.4"` is fractionally more accurate than `317x498`. For whole-number dims — + * every well-formed tag — the two are the same number. + * + * Returning null matters as much as the value. [parse] only rejects the literal string + * `"0x0"`, so a tag can still arrive with no usable shape at all, and [aspectRatio] then + * returns `0/0`, which is `NaN`. Compose's `Modifier.aspectRatio` throws + * `IllegalArgumentException` on `NaN` and on `0f`, and clamping does not rescue either: every + * comparison against `NaN` is false, so `coerceAtLeast` returns it unchanged. A tag any relay + * can carry would otherwise crash the composition around it. */ - fun aspectRatioOrNull(): Float? = if (hasSize()) aspectRatio() else null + fun aspectRatioOrNull(): Float? = declaredRatio ?: if (hasSize()) aspectRatio() else null fun hasSize() = width > 0 && height > 0 @@ -74,15 +88,28 @@ class DimensionTag( // Some clients (e.g. Primal) emit floating-point dimensions like "317.0x498.0" // in NIP-92 imeta tags. Parse as Double and truncate to keep those tags usable // for pre-load layout reservation. - val width = parts[0].toDouble().toInt() - val height = parts[1].toDouble().toInt() + val width = parts[0].toDouble() + val height = parts[1].toDouble() - DimensionTag(width, height) + DimensionTag(width.toInt(), height.toInt(), declaredRatio(width, height)) } catch (e: Exception) { null } } + /** + * The declared ratio, or null when the declared size cannot describe a shape at all. + * Computed before the truncation to whole pixels, which is the whole point: a dim under + * `1` on either axis truncates to `0` and takes its shape with it. + */ + private fun declaredRatio( + width: Double, + height: Double, + ): Float? { + if (width <= 0.0 || height <= 0.0) return null + return (width / height).toFloat().takeIf { it.isFinite() && it > 0f } + } + fun assemble(dim: DimensionTag) = arrayOf(TAG_NAME, dim.toString()) } } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/tags/DimensionTagTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/tags/DimensionTagTest.kt index 784eeca2c8..66e2d38f89 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/tags/DimensionTagTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/tags/DimensionTagTest.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.nip94FileMetadata.tags import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue @@ -76,14 +77,55 @@ class DimensionTagTest { /** * [rejectsZeroByZero] passes by comparing the string, so the float path of the Primal - * tolerance above reaches the same 0x0 value by truncation and is kept. + * tolerance above reaches the same 0x0 pixel counts by truncation and is kept. */ @Test - fun zeroSizedFloatsSlipPastTheZeroByZeroRejection() { + fun subPixelFloatsTruncateToNoPixels() { val tag = DimensionTag.parse("0.4x0.4") assertNotNull(tag) assertEquals(0, tag.width) assertEquals(0, tag.height) + assertFalse(tag.hasSize()) + } + + /** + * ...but the shape the author declared survives that truncation. A square is a square whether + * it was declared as "0.4x0.4" or "400x400"; only the pixel counts are unusable. + */ + @Test + fun subPixelFloatsKeepTheDeclaredShape() { + assertEquals(1f, DimensionTag.parse("0.4x0.4")?.aspectRatioOrNull()) + assertEquals(0.75f, DimensionTag.parse("0.75x1")?.aspectRatioOrNull()) + assertEquals(2f, DimensionTag.parse("0.5x0.25")?.aspectRatioOrNull()) + } + + /** + * The declared ratio wins over the truncated one wherever they differ, being the more faithful + * of the two. They differ only for fractional dims — for whole numbers it is the same number. + */ + @Test + fun declaredRatioBeatsTheTruncatedOne() { + val tag = DimensionTag.parse("317.9x498.4") + assertNotNull(tag) + assertEquals((317.9 / 498.4).toFloat(), tag.aspectRatioOrNull()) + assertEquals(317f / 498f, tag.aspectRatio()) + } + + @Test + fun wholeNumberDimsAreUnaffected() { + val tag = DimensionTag.parse("1920x1080") + assertNotNull(tag) + assertEquals(tag.aspectRatio(), tag.aspectRatioOrNull()) + } + + /** + * The declared ratio is derived from the tag text, so it is not serialized — a tag rebuilt + * from its width and height falls back to the pixel counts rather than carrying a stale one. + */ + @Test + fun aDirectlyConstructedTagFallsBackToPixelCounts() { + assertEquals(1920f / 1080f, DimensionTag(1920, 1080).aspectRatioOrNull()) + assertNull(DimensionTag(0, 0).aspectRatioOrNull()) } /** @@ -96,9 +138,15 @@ class DimensionTagTest { assertEquals(0f, DimensionTag(0, 100).aspectRatio()) } + /** + * A fractional dim keeps its shape (see [subPixelFloatsKeepTheDeclaredShape]); one that + * declares no shape at all still has to come back null, since `Modifier.aspectRatio` throws on + * everything this would otherwise produce. + */ @Test - fun aspectRatioOrNullRefusesEverySizeALayoutCannotUse() { - assertNull(DimensionTag.parse("0.4x0.4")?.aspectRatioOrNull()) + fun aspectRatioOrNullRefusesEveryShapeALayoutCannotUse() { + assertNull(DimensionTag.parse("0x5")?.aspectRatioOrNull()) + assertNull(DimensionTag.parse("-3x4")?.aspectRatioOrNull()) assertNull(DimensionTag(0, 0).aspectRatioOrNull()) assertNull(DimensionTag(100, 0).aspectRatioOrNull()) assertNull(DimensionTag(0, 100).aspectRatioOrNull()) From a12e62265cc1cc0705c650c2b7104bd0f545bf5a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 18:38:57 +0000 Subject: [PATCH 6/7] test: pin DimensionTag against non-numeric doubles from relays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "NaN" and "Infinity" are legal input to Kotlin's String.toDouble(), so both reach the ratio maths from any relay-carried dim tag. Worth pinning because the `<= 0.0` rejection cannot stop NaN — every comparison against it is false — leaving the finite check as the only thing holding that line. Verified rather than assumed: NaN yields no shape, and Infinity saturates to Int.MAX_VALUE on both axes for a usable 1:1. Neither produces a shape Modifier.aspectRatio would throw on. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019DjHVxiXFncNhoKuNgtSio --- .../nip94FileMetadata/tags/DimensionTagTest.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/tags/DimensionTagTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/tags/DimensionTagTest.kt index 66e2d38f89..abec85a647 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/tags/DimensionTagTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip94FileMetadata/tags/DimensionTagTest.kt @@ -157,4 +157,19 @@ class DimensionTagTest { fun aspectRatioOrNullKeepsUsableSizes() { assertEquals(317f / 498f, DimensionTag(317, 498).aspectRatioOrNull()) } + + /** + * `"NaN"` and `"Infinity"` are legal input to Kotlin's `String.toDouble()` and a relay can + * carry either, so both reach the ratio maths. The `<= 0.0` rejection cannot stop `NaN` — + * every comparison against it is false — which is why the finite check is the one holding the + * line. Whatever comes out, it is never a shape a layout would throw on. + */ + @Test + fun nonNumericDoublesNeverProduceAnUnusableShape() { + assertNull(DimensionTag.parse("NaNxNaN")?.aspectRatioOrNull()) + assertNull(DimensionTag.parse("0.4xNaN")?.aspectRatioOrNull()) + + // Infinity saturates to Int.MAX_VALUE on both axes: garbage in, square out, never a throw. + assertEquals(1f, DimensionTag.parse("InfinityxInfinity")?.aspectRatioOrNull()) + } } From eb97dd1174568363e6841be867b0994d8a279e90 Mon Sep 17 00:00:00 2001 From: vitorpamplona <532031+vitorpamplona@users.noreply.github.com> Date: Thu, 13 Aug 2026 19:06:24 +0000 Subject: [PATCH 7/7] chore: sync Crowdin translations and seed translator npub placeholders --- amethyst/src/main/res/values-cs/strings.xml | 378 +++++++++--------- .../src/main/res/values-de-rDE/strings.xml | 314 +++++++-------- .../src/main/res/values-pt-rBR/strings.xml | 314 +++++++-------- .../src/main/res/values-sv-rSE/strings.xml | 314 +++++++-------- .../composeResources/values-cs/strings.xml | 68 ++-- .../values-de-rDE/strings.xml | 68 ++-- .../values-pt-rBR/strings.xml | 68 ++-- .../values-sv-rSE/strings.xml | 68 ++-- docs/changelog/translators.json | 24 +- 9 files changed, 808 insertions(+), 808 deletions(-) diff --git a/amethyst/src/main/res/values-cs/strings.xml b/amethyst/src/main/res/values-cs/strings.xml index 4dfa7dd4c3..8d18e5a564 100644 --- a/amethyst/src/main/res/values-cs/strings.xml +++ b/amethyst/src/main/res/values-cs/strings.xml @@ -54,6 +54,8 @@ Násilí Neznámý autor Kopírovat text + Kopírovat originál + Kopírovat překlad Kopírovat ID autora Kopírovat ID poznámky Kopírovat surový JSON @@ -318,6 +320,18 @@ Tento zvací odkaz je neplatný nebo jej nelze otevřít tímto účtem. Tento zvací odkaz nelze otevřít. Může být zastaralý nebo již nahrazený novějším, případně vytvořený novější verzí aplikace. Vyžádejte si nový zvací odkaz. Tento zvací odkaz byl zrušen a již jej nelze použít. Vyžádejte si nový. + Tato komunita vás odebrala. Odkaz stále funguje, ale její seznam členů vás nevpustí. + Odkazy pro pozvání + Odkazy pro pozvání… + Vytvořeno %1$s + Pro tuto komunitu jste zatím nevytvořil žádné odkazy pro pozvání. Odkazy vytvořené jinými správci se spravují na jejich vlastních zařízeních. + Vaše odkazy pro pozvání se nepodařilo načíst, takže žádný z nich nyní nelze zrušit. Zkontrolujte připojení a zkuste to znovu. + Zrušit odkaz + Zrušit tento odkaz? + Kdokoli, kdo tento odkaz stále má, se už nebude moci připojit. Lidé, kteří se přes něj již připojili, v komunitě zůstávají. Tuto akci nelze vzít zpět. + Zrušit + Odkaz pro pozvání byl zrušen. + Odkaz se nepodařilo zrušit. Zkontrolujte připojení a zkuste to znovu. Platnost tohoto pozvánkového odkazu vypršela a nelze jej již použít. Vyžádejte si nový odkaz. Název komunity se zobrazí až po připojení Připojením se spojíte s relayi této pozvánky, zveřejníte oznámení o připojení podepsané vaším účtem a přidáte komunitu do svého seznamu. Dokud nestisknete Připojit se, nic se neodešle. @@ -515,6 +529,11 @@ \n- **Udržujte** bezpečnou zálohu vašeho tajného klíče pro obnovení účtu. Doporučujeme použití správce hesel. Pro další zabezpečení můžete svůj klíč zašifrovat heslem. Tento klíč začíná s **ncryptsec1** a nelze ho použít bez vašeho hesla. \n\nPokud zapomenete heslo, nebudete moci obnovit svůj klíč. + Zálohujte si klíče + Váš tajný klíč je jediný způsob, jak se k tomuto účtu dostat. Pokud ho ztratíte, nelze ho už nikdy obnovit. Uložte si ho někam do bezpečí. + Zálohovat nyní + Uložil jsem si je + Zavřít Nepodařilo se zašifrovat váš privátní klíč Tajný klíč (nsec) zkopírován do schránky Zkopírovat můj tajný klíč @@ -1150,12 +1169,71 @@ + Přihlásit se bez ptaní, když… + …jde o mé relé nebo o místnost, do které jsem vstoupil + …čtu někoho, koho sleduji + …píšu někomu, koho sleduji + …píšu komukoli jinému + Přihlásit se jako %1$s? + se ptá, kdo jste + Dokud se nepřihlásíte, nepřijme vaši zprávu pro %1$s. + %1$s se o vaší odpovědi nedozví, dokud se nepřihlásíte. + Dokud se nepřihlásíte, nebude vydávat příspěvky od %1$s. + Dokud se nepřihlásíte, nepřijme váš příspěvek v %1$s. + Dokud se nepřihlásíte, nebude vydávat %1$s. + Dokud se nepřihlásíte, nebude vydávat odpovědi, zapy ani zprávy poslané vám. + Dokud se nepřihlásíte, nebude vydávat zbytek této konverzace. + Dokud se nepřihlásíte, nebude vydávat zbytek vaší konverzace s %1$s. + Toto je jedno z vašich vlastních relé a žádá o přihlášení každého. + Žádá o přihlášení každého. Amethyst nedokáže zjistit, co zadržuje. + někdo, koho jste ještě nenačetl + + %1$s a %2$d další + %1$s a %2$d další + %1$s a %2$d dalšího + %1$s a %2$d dalších + + + Zadržuje také %1$d další věc. + Zadržuje také %1$d další věci. + Zadržuje také %1$d další věci. + Zadržuje také %1$d dalších věcí. + + Zapamatovat pro toto relé + Přihlásit se + Teď ne + Nikdy nepovolovat + Jak se Amethyst rozhoduje + Výjimky + Zatím žádné výjimky. Všechna relé se řídí pravidly výše. + Vždy, nastaveno vámi + Nikdy, nastaveno vámi + Vždy + Nikdy + Odebrat výjimku + Výjimka odebrána. %1$s se opět řídí vašimi pravidly. + Zpět + Blokováno vaším seznamem blokovaných + Amethyst se k blokovaným relé nikdy nepřihlašuje. + Nic není blokováno. K relé, která zablokujete, se Amethyst nikdy nepřihlásí, ať zde nastavíte cokoli. + Nedávná přihlášení + Zatím vás o přihlášení nepožádalo žádné relé. + před %1$s + vaše zprávy + vaše odpovědi + lidé, které čtete + místnost, do které přispíváte + místnost, kterou čtete + vaše doručená pošta + konverzace + vaše vlastní relé + toto relé Zdroj: %1$s v%1$s Stáhnout @@ -1534,6 +1612,58 @@ Viditelné pro Zatím žádní příjemci: tuto poznámku uvidíš jen ty. Přidej lidi, se kterými ji chceš sdílet. + Spravovat, kdo to uvidí + Jen vy — vyberte, kdo další to uvidí + Přidejte lidi, kterým přijde upozornění + %1$s a %2$s + %1$s · %2$s + Odebrat všechny přidané ze seznamu %1$s + Poslat soukromě + Přidat lidi + Hledat lidi a seznamy + Zatím nemáte žádné seznamy lidí ani balíčky sledování. Vyhledáváním výše můžete někoho přidat přímo. + %1$d · %2$d soukromě + Překročen limit + Již přidáno + Ztlumeno + Soukromý člen + Každý, kdo je v této poznámce, vidí celý seznam příjemců. + + %1$s, %2$s a %3$d další + %1$s, %2$s a %3$d další + %1$s, %2$s a %3$d dalšího + %1$s, %2$s a %3$d dalších + + + Tato poznámka bude zašifrována a doručena jako %1$d samostatná zpráva, jedna pro každou osobu. S externím podepisovačem to znamená jedno schválení za každou. + Tato poznámka bude zašifrována a doručena jako %1$d samostatné zprávy, jedna pro každou osobu. S externím podepisovačem to znamená jedno schválení za každou. + Tato poznámka bude zašifrována a doručena jako %1$d samostatné zprávy, jedna pro každou osobu. S externím podepisovačem to znamená jedno schválení za každou. + Tato poznámka bude zašifrována a doručena jako %1$d samostatných zpráv, jedna pro každou osobu. S externím podepisovačem to znamená jedno schválení za každou. + + + Upozornění na tento příspěvek dostane %1$d osoba. + Upozornění na tento příspěvek dostanou %1$d osoby. + Upozornění na tento příspěvek dostane %1$d osoby. + Upozornění na tento příspěvek dostane %1$d osob. + + + To by byla %1$d osoba. Amethyst přidá najednou nejvýše %2$d — několik jich odznačte. + To by byly %1$d osoby. Amethyst přidá najednou nejvýše %2$d — několik jich odznačte. + To by bylo %1$d osoby. Amethyst přidá najednou nejvýše %2$d — několik jich odznačte. + To by bylo %1$d osob. Amethyst přidá najednou nejvýše %2$d — několik jich odznačte. + + + Přidat %1$d osobu + Přidat %1$d osoby + Přidat %1$d osoby + Přidat %1$d osob + + + Přesto přidat %1$d osobu + Přesto přidat %1$d osoby + Přesto přidat %1$d osoby + Přesto přidat %1$d osob + Přidat Odebrat uživatele z oznámení Upozorňování zapnuto. Klepnutím ztlumíte upozornění pro tohoto uživatele @@ -4028,6 +4158,65 @@ Sociální důkaz Odeslat Zobrazit výsledky + Výsledky ankety + Načítá se anketa… + Otevřená + Končí za %1$s + Skončila před %1$s + Všechny možnosti + Váš hlas + + · %1$d výběr + · %1$d výběry + · %1$d výběru + · %1$d výběrů + + Zatím žádné hlasy + Zjišťuje se, kolik hlasů existuje… + Načteno %1$d z %2$d odpovědí z %3$d relé. + Načteno %1$d z přibližně %2$d odpovědí z %3$d relé. + + hlasující + hlasující + hlasujícího + hlasujících + + + %1$d hlas › + %1$d hlasy › + %1$d hlasu › + %1$d hlasů › + + + %1$d hlasující + %1$d hlasující + %1$d hlasujícího + %1$d hlasujících + + + %1$d hlas dorazil po uzavření ankety a je vyloučen. + %1$d hlasy dorazily po uzavření ankety a jsou vyloučeny. + %1$d hlasu dorazilo po uzavření ankety a je vyloučeno. + %1$d hlasů dorazilo po uzavření ankety a je vyloučeno. + + + %1$d hlas má datum před zveřejněním ankety a je vyloučen. + %1$d hlasy mají datum před zveřejněním ankety a jsou vyloučeny. + %1$d hlasu má datum před zveřejněním ankety a je vyloučeno. + %1$d hlasů má datum před zveřejněním ankety a je vyloučeno. + + + %1$d odpověď nevybrala platnou možnost a je vyloučena. + %1$d odpovědi nevybraly platnou možnost a jsou vyloučeny. + %1$d odpovědi nevybralo platnou možnost a je vyloučeno. + %1$d odpovědí nevybralo platnou možnost a je vyloučeno. + + + %1$d hlasující je skryt vaším seznamem ztlumených. + %1$d hlasující jsou skryti vaším seznamem ztlumených. + %1$d hlasujícího je skryto vaším seznamem ztlumených. + %1$d hlasujících je skryto vaším seznamem ztlumených. + Restartovat Přijmout Odmítnout @@ -4895,193 +5084,4 @@ URL avataru (volitelné) Publikování… Publikovat personu - Již přidáno - Ztlumeno - Soukromý člen - %1$d · %2$d soukromě - Jen vy — vyberte, kdo další to uvidí - Přidejte lidi, kterým přijde upozornění - %1$s · %2$s - Odebrat všechny přidané ze seznamu %1$s - Spravovat, kdo to uvidí - Překročen limit - Každý, kdo je v této poznámce, vidí celý seznam příjemců. - Poslat soukromě - Zatím nemáte žádné seznamy lidí ani balíčky sledování. Vyhledáváním výše můžete někoho přidat přímo. - Hledat lidi a seznamy - Přidat lidi - %1$s a %2$s - Uložil jsem si je - Zálohovat nyní - Váš tajný klíč je jediný způsob, jak se k tomuto účtu dostat. Pokud ho ztratíte, nelze ho už nikdy obnovit. Uložte si ho někam do bezpečí. - Zavřít - Zálohujte si klíče - Tato komunita vás odebrala. Odkaz stále funguje, ale její seznam členů vás nevpustí. - Odkazy pro pozvání… - Vytvořeno %1$s - Pro tuto komunitu jste zatím nevytvořil žádné odkazy pro pozvání. Odkazy vytvořené jinými správci se spravují na jejich vlastních zařízeních. - Odkazy pro pozvání - Vaše odkazy pro pozvání se nepodařilo načíst, takže žádný z nich nyní nelze zrušit. Zkontrolujte připojení a zkuste to znovu. - Zrušit odkaz - Zrušit - Kdokoli, kdo tento odkaz stále má, se už nebude moci připojit. Lidé, kteří se přes něj již připojili, v komunitě zůstávají. Tuto akci nelze vzít zpět. - Zrušit tento odkaz? - Odkaz se nepodařilo zrušit. Zkontrolujte připojení a zkuste to znovu. - Odkaz pro pozvání byl zrušen. - Kopírovat originál - Kopírovat překlad - Všechny možnosti - Zjišťuje se, kolik hlasů existuje… - Končí za %1$s - Skončila před %1$s - Načítá se anketa… - Zatím žádné hlasy - Otevřená - Načteno %1$d z %2$d odpovědí z %3$d relé. - Načteno %1$d z přibližně %2$d odpovědí z %3$d relé. - Výsledky ankety - Váš hlas - před %1$s - Přihlásit se bez ptaní, když… - …píšu někomu, koho sleduji - …píšu komukoli jinému - …jde o mé relé nebo o místnost, do které jsem vstoupil - …čtu někoho, koho sleduji - Amethyst se k blokovaným relé nikdy nepřihlašuje. - Blokováno vaším seznamem blokovaných - Vždy, nastaveno vámi - Nikdy, nastaveno vámi - Výjimka odebrána. %1$s se opět řídí vašimi pravidly. - Výjimky - Jak se Amethyst rozhoduje - Přihlásit se - Přihlásit se jako %1$s? - Nikdy nepovolovat - Nic není blokováno. K relé, která zablokujete, se Amethyst nikdy nepřihlásí, ať zde nastavíte cokoli. - Zatím žádné výjimky. Všechna relé se řídí pravidly výše. - Zatím vás o přihlášení nepožádalo žádné relé. - Teď ne - vaše doručená pošta - vaše vlastní relé - vaše odpovědi - toto relé - místnost, do které přispíváte - lidé, které čtete - místnost, kterou čtete - vaše zprávy - konverzace - Nedávná přihlášení - se ptá, kdo jste - Zapamatovat pro toto relé - Odebrat výjimku - Vždy - Nikdy - někdo, koho jste ještě nenačetl - Zpět - Dokud se nepřihlásíte, nebude vydávat odpovědi, zapy ani zprávy poslané vám. - Toto je jedno z vašich vlastních relé a žádá o přihlášení každého. - %1$s se o vaší odpovědi nedozví, dokud se nepřihlásíte. - Žádá o přihlášení každého. Amethyst nedokáže zjistit, co zadržuje. - Dokud se nepřihlásíte, nepřijme váš příspěvek v %1$s. - Dokud se nepřihlásíte, nebude vydávat příspěvky od %1$s. - Dokud se nepřihlásíte, nebude vydávat %1$s. - Dokud se nepřihlásíte, nepřijme vaši zprávu pro %1$s. - Dokud se nepřihlásíte, nebude vydávat zbytek této konverzace. - Dokud se nepřihlásíte, nebude vydávat zbytek vaší konverzace s %1$s. - - Přesto přidat %1$d osobu - Přesto přidat %1$d osoby - Přesto přidat %1$d osoby - Přesto přidat %1$d osob - - - Přidat %1$d osobu - Přidat %1$d osoby - Přidat %1$d osoby - Přidat %1$d osob - - - To by byla %1$d osoba. Amethyst přidá najednou nejvýše %2$d — několik jich odznačte. - To by byly %1$d osoby. Amethyst přidá najednou nejvýše %2$d — několik jich odznačte. - To by bylo %1$d osoby. Amethyst přidá najednou nejvýše %2$d — několik jich odznačte. - To by bylo %1$d osob. Amethyst přidá najednou nejvýše %2$d — několik jich odznačte. - - - Tato poznámka bude zašifrována a doručena jako %1$d samostatná zpráva, jedna pro každou osobu. S externím podepisovačem to znamená jedno schválení za každou. - Tato poznámka bude zašifrována a doručena jako %1$d samostatné zprávy, jedna pro každou osobu. S externím podepisovačem to znamená jedno schválení za každou. - Tato poznámka bude zašifrována a doručena jako %1$d samostatné zprávy, jedna pro každou osobu. S externím podepisovačem to znamená jedno schválení za každou. - Tato poznámka bude zašifrována a doručena jako %1$d samostatných zpráv, jedna pro každou osobu. S externím podepisovačem to znamená jedno schválení za každou. - - - Upozornění na tento příspěvek dostane %1$d osoba. - Upozornění na tento příspěvek dostanou %1$d osoby. - Upozornění na tento příspěvek dostane %1$d osoby. - Upozornění na tento příspěvek dostane %1$d osob. - - - %1$s, %2$s a %3$d další - %1$s, %2$s a %3$d další - %1$s, %2$s a %3$d dalšího - %1$s, %2$s a %3$d dalších - - - %1$d hlas má datum před zveřejněním ankety a je vyloučen. - %1$d hlasy mají datum před zveřejněním ankety a jsou vyloučeny. - %1$d hlasu má datum před zveřejněním ankety a je vyloučeno. - %1$d hlasů má datum před zveřejněním ankety a je vyloučeno. - - - %1$d hlasující je skryt vaším seznamem ztlumených. - %1$d hlasující jsou skryti vaším seznamem ztlumených. - %1$d hlasujícího je skryto vaším seznamem ztlumených. - %1$d hlasujících je skryto vaším seznamem ztlumených. - - - %1$d odpověď nevybrala platnou možnost a je vyloučena. - %1$d odpovědi nevybraly platnou možnost a jsou vyloučeny. - %1$d odpovědi nevybralo platnou možnost a je vyloučeno. - %1$d odpovědí nevybralo platnou možnost a je vyloučeno. - - - %1$d hlas dorazil po uzavření ankety a je vyloučen. - %1$d hlasy dorazily po uzavření ankety a jsou vyloučeny. - %1$d hlasu dorazilo po uzavření ankety a je vyloučeno. - %1$d hlasů dorazilo po uzavření ankety a je vyloučeno. - - - %1$d hlasující - %1$d hlasující - %1$d hlasujícího - %1$d hlasujících - - - · %1$d výběr - · %1$d výběry - · %1$d výběru - · %1$d výběrů - - - %1$d hlas › - %1$d hlasy › - %1$d hlasu › - %1$d hlasů › - - - hlasující - hlasující - hlasujícího - hlasujících - - - Zadržuje také %1$d další věc. - Zadržuje také %1$d další věci. - Zadržuje také %1$d další věci. - Zadržuje také %1$d dalších věcí. - - - %1$s a %2$d další - %1$s a %2$d další - %1$s a %2$d dalšího - %1$s a %2$d dalších - diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index ff6691a34c..374498f235 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -46,6 +46,8 @@ Gewalt Unbekannter Autor Text kopieren + Original kopieren + Übersetzung kopieren Autor-ID kopieren Beitrags-ID kopieren JSON kopieren @@ -310,6 +312,18 @@ Dieser Einladungslink ist ungültig oder kann mit diesem Konto nicht geöffnet werden. Dieser Einladungslink kann nicht geöffnet werden. Er ist möglicherweise veraltet, wurde bereits durch einen neueren ersetzt oder mit einer neueren Version der App erstellt. Bitte um einen neuen Einladungslink. Dieser Einladungslink wurde widerrufen und kann nicht mehr verwendet werden. Bitte um einen neuen. + Diese Community hat dich entfernt. Der Link funktioniert noch, aber ihre Mitgliederliste lässt dich nicht zu. + Einladungslinks + Einladungslinks… + Erstellt %1$s + Du hast für diese Community noch keine Einladungslinks erstellt. Links, die andere Admins erstellt haben, werden auf deren eigenen Geräten verwaltet. + Deine Einladungslinks konnten nicht geladen werden, daher lässt sich gerade keiner widerrufen. Prüfe deine Verbindung und versuche es erneut. + Link widerrufen + Diesen Link widerrufen? + Wer diesen Link noch hat, kann damit nicht mehr beitreten. Personen, die bereits damit beigetreten sind, bleiben in der Community. Das lässt sich nicht rückgängig machen. + Widerrufen + Einladungslink widerrufen. + Der Link konnte nicht widerrufen werden. Prüfe deine Verbindung und versuche es erneut. Dieser Einladungslink ist abgelaufen und kann nicht mehr verwendet werden. Bitte um einen neuen Link. Der Name der Community wird erst nach dem Beitritt angezeigt Beim Beitreten verbindest du dich mit den Relays dieser Einladung, veröffentlichst eine mit deinem Konto signierte Beitrittsankündigung und fügst die Community deiner Liste hinzu. Es wird nichts gesendet, bis du auf Beitreten tippst. @@ -495,6 +509,11 @@ \n- **Bewahre** eine sichere Kopie deines privaten Schlüssels zur Kontowiederherstellung auf. Wir empfehlen die Verwendung eines Passwort-Managers. Für zusätzliche Sicherheit kannst du deinen Schlüssel mit einem Passwort verschlüsseln. Dieser Schlüssel beginnt mit **ncryptsec1** und kann ohne dein Passwort nicht verwendet werden. \n\nWenn du dein Passwort verlierst, kannst du deinen Schlüssel nicht wiederherstellen. + Sichere deine Schlüssel + Dein geheimer Schlüssel ist der einzige Weg zu diesem Konto. Wenn du ihn verlierst, kann er nie wiederhergestellt werden. Speichere ihn jetzt an einem sicheren Ort. + Jetzt sichern + Habe ich gespeichert + Schließen Fehler beim Verschlüsseln Ihres privaten Schlüssels Geheimer Schlüssel (nsec) in die Zwischenablage kopiert Meinen geheimen Schlüssel kopieren @@ -1090,12 +1109,67 @@ + Ohne Nachfrage anmelden, wenn… + …es mein Relay ist oder ein Raum, dem ich beigetreten bin + …ich jemanden lese, dem ich folge + …ich jemandem schreibe, dem ich folge + …ich jemand anderem schreibe + Als %1$s anmelden? + fragt, wer du bist + Es nimmt deine Nachricht an %1$s nicht an, solange du dich nicht anmeldest. + %1$s erfährt nichts von deiner Antwort, solange du dich nicht anmeldest. + Es liefert keine Beiträge von %1$s, solange du dich nicht anmeldest. + Es nimmt deinen Beitrag in %1$s nicht an, solange du dich nicht anmeldest. + Es liefert %1$s nicht, solange du dich nicht anmeldest. + Es liefert dir keine Antworten, Zaps und Nachrichten, die an dich gesendet wurden, solange du dich nicht anmeldest. + Es liefert den Rest dieser Unterhaltung nicht, solange du dich nicht anmeldest. + Es liefert den Rest deiner Unterhaltung mit %1$s nicht, solange du dich nicht anmeldest. + Das ist eines deiner eigenen Relays, und es fordert alle zur Anmeldung auf. + Es fordert alle zur Anmeldung auf. Amethyst kann nicht erkennen, was es zurückhält. + jemand, den du noch nicht geladen hast + + %1$s und %2$d weitere Person + %1$s und %2$d weitere Personen + + + Es hält außerdem %1$d weitere Sache zurück. + Es hält außerdem %1$d weitere Dinge zurück. + + Für dieses Relay merken + Anmelden + Jetzt nicht + Nie erlauben + Wie Amethyst entscheidet + Ausnahmen + Noch keine Ausnahmen. Jedes Relay folgt den Regeln oben. + Immer, von dir festgelegt + Nie, von dir festgelegt + Immer + Nie + Ausnahme entfernen + Ausnahme entfernt. %1$s folgt wieder deinen Regeln. + Rückgängig + Von deiner Blockliste blockiert + Amethyst meldet sich nie bei blockierten Relays an. + Nichts blockiert. Bei Relays, die du blockierst, meldet sich Amethyst nie an — egal, was du hier einstellst. + Letzte Anmeldungen + Noch hat dich kein Relay zur Anmeldung aufgefordert. + vor %1$s + deine Nachrichten + deine Antworten + Personen, die du liest + ein Raum, in dem du postest + ein Raum, den du liest + dein Posteingang + eine Unterhaltung + dein eigenes Relay + dieses Relay Quelle: %1$s v%1$s Herunterladen @@ -1464,6 +1538,46 @@ Sichtbar für Noch keine Empfänger: Nur du kannst diese Notiz sehen. Füge Personen hinzu, um sie zu teilen. + Verwalten, wer das sieht + Nur du — wähle, wer das sonst noch sehen darf + Personen hinzufügen, die benachrichtigt werden + %1$s und %2$s + %1$s · %2$s + Alle aus %1$s hinzugefügten Personen entfernen + Privat senden + Personen hinzufügen + Personen und Listen suchen + Du hast noch keine Personenlisten oder Follow-Pakete. Suche oben, um jemanden direkt hinzuzufügen. + %1$d · %2$d privat + Über dem Limit + Bereits hinzugefügt + Stummgeschaltet + Privates Mitglied + Alle in dieser Notiz können die vollständige Empfängerliste sehen. + + %1$s, %2$s und %3$d weitere Person + %1$s, %2$s und %3$d weitere Personen + + + Diese Notiz wird verschlüsselt und %1$d Mal einzeln zugestellt, einmal pro Person. Mit einem externen Signierer ist das je eine Freigabe. + Diese Notiz wird verschlüsselt und %1$d Mal einzeln zugestellt, einmal pro Person. Mit einem externen Signierer ist das je eine Freigabe. + + + %1$d Person erhält eine Benachrichtigung zu diesem Beitrag. + %1$d Personen erhalten eine Benachrichtigung zu diesem Beitrag. + + + Das wäre %1$d Person. Amethyst fügt höchstens %2$d auf einmal hinzu — wähle ein paar ab. + Das wären %1$d Personen. Amethyst fügt höchstens %2$d auf einmal hinzu — wähle ein paar ab. + + + %1$d Person hinzufügen + %1$d Personen hinzufügen + + + Trotzdem %1$d Person hinzufügen + Trotzdem %1$d Personen hinzufügen + Hinzufügen Nutzer aus Benachrichtigungen entfernen Benachrichtigungen aktiv. Tippen, um Benachrichtigungen für diesen Nutzer stummzuschalten @@ -3888,6 +4002,49 @@ Sozialer Nachweis Absenden Ergebnisse anzeigen + Umfrageergebnisse + Umfrage wird geladen… + Offen + Endet in %1$s + Vor %1$s beendet + Alle Optionen + Deine Stimme + + · %1$d Auswahl + · %1$d Auswahlen + + Noch keine Stimmen + Es wird geprüft, wie viele Stimmen existieren… + %1$d von %2$d Antworten von %3$d Relays geladen. + %1$d von etwa %2$d Antworten von %3$d Relays geladen. + + abstimmende Person + abstimmende Personen + + + %1$d Stimme › + %1$d Stimmen › + + + %1$d abstimmende Person + %1$d abstimmende Personen + + + %1$d Stimme ist nach dem Ende der Umfrage eingegangen und wird ausgeschlossen. + %1$d Stimmen sind nach dem Ende der Umfrage eingegangen und werden ausgeschlossen. + + + %1$d Stimme ist auf einen Zeitpunkt vor der Veröffentlichung der Umfrage datiert und wird ausgeschlossen. + %1$d Stimmen sind auf einen Zeitpunkt vor der Veröffentlichung der Umfrage datiert und werden ausgeschlossen. + + + %1$d Antwort hat keine gültige Option gewählt und wird ausgeschlossen. + %1$d Antworten haben keine gültige Option gewählt und werden ausgeschlossen. + + + %1$d abstimmende Person wird durch deine Stummschaltliste ausgeblendet. + %1$d abstimmende Personen werden durch deine Stummschaltliste ausgeblendet. + Neu starten Annehmen Ablehnen @@ -4715,161 +4872,4 @@ Avatar-URL (optional) Wird veröffentlicht… Persona veröffentlichen - Bereits hinzugefügt - Stummgeschaltet - Privates Mitglied - %1$d · %2$d privat - Nur du — wähle, wer das sonst noch sehen darf - Personen hinzufügen, die benachrichtigt werden - %1$s · %2$s - Alle aus %1$s hinzugefügten Personen entfernen - Verwalten, wer das sieht - Über dem Limit - Alle in dieser Notiz können die vollständige Empfängerliste sehen. - Privat senden - Du hast noch keine Personenlisten oder Follow-Pakete. Suche oben, um jemanden direkt hinzuzufügen. - Personen und Listen suchen - Personen hinzufügen - %1$s und %2$s - Habe ich gespeichert - Jetzt sichern - Dein geheimer Schlüssel ist der einzige Weg zu diesem Konto. Wenn du ihn verlierst, kann er nie wiederhergestellt werden. Speichere ihn jetzt an einem sicheren Ort. - Schließen - Sichere deine Schlüssel - Diese Community hat dich entfernt. Der Link funktioniert noch, aber ihre Mitgliederliste lässt dich nicht zu. - Einladungslinks… - Erstellt %1$s - Du hast für diese Community noch keine Einladungslinks erstellt. Links, die andere Admins erstellt haben, werden auf deren eigenen Geräten verwaltet. - Einladungslinks - Deine Einladungslinks konnten nicht geladen werden, daher lässt sich gerade keiner widerrufen. Prüfe deine Verbindung und versuche es erneut. - Link widerrufen - Widerrufen - Wer diesen Link noch hat, kann damit nicht mehr beitreten. Personen, die bereits damit beigetreten sind, bleiben in der Community. Das lässt sich nicht rückgängig machen. - Diesen Link widerrufen? - Der Link konnte nicht widerrufen werden. Prüfe deine Verbindung und versuche es erneut. - Einladungslink widerrufen. - Original kopieren - Übersetzung kopieren - Alle Optionen - Es wird geprüft, wie viele Stimmen existieren… - Endet in %1$s - Vor %1$s beendet - Umfrage wird geladen… - Noch keine Stimmen - Offen - %1$d von %2$d Antworten von %3$d Relays geladen. - %1$d von etwa %2$d Antworten von %3$d Relays geladen. - Umfrageergebnisse - Deine Stimme - vor %1$s - Ohne Nachfrage anmelden, wenn… - …ich jemandem schreibe, dem ich folge - …ich jemand anderem schreibe - …es mein Relay ist oder ein Raum, dem ich beigetreten bin - …ich jemanden lese, dem ich folge - Amethyst meldet sich nie bei blockierten Relays an. - Von deiner Blockliste blockiert - Immer, von dir festgelegt - Nie, von dir festgelegt - Ausnahme entfernt. %1$s folgt wieder deinen Regeln. - Ausnahmen - Wie Amethyst entscheidet - Anmelden - Als %1$s anmelden? - Nie erlauben - Nichts blockiert. Bei Relays, die du blockierst, meldet sich Amethyst nie an — egal, was du hier einstellst. - Noch keine Ausnahmen. Jedes Relay folgt den Regeln oben. - Noch hat dich kein Relay zur Anmeldung aufgefordert. - Jetzt nicht - dein Posteingang - dein eigenes Relay - deine Antworten - dieses Relay - ein Raum, in dem du postest - Personen, die du liest - ein Raum, den du liest - deine Nachrichten - eine Unterhaltung - Letzte Anmeldungen - fragt, wer du bist - Für dieses Relay merken - Ausnahme entfernen - Immer - Nie - jemand, den du noch nicht geladen hast - Rückgängig - Es liefert dir keine Antworten, Zaps und Nachrichten, die an dich gesendet wurden, solange du dich nicht anmeldest. - Das ist eines deiner eigenen Relays, und es fordert alle zur Anmeldung auf. - %1$s erfährt nichts von deiner Antwort, solange du dich nicht anmeldest. - Es fordert alle zur Anmeldung auf. Amethyst kann nicht erkennen, was es zurückhält. - Es nimmt deinen Beitrag in %1$s nicht an, solange du dich nicht anmeldest. - Es liefert keine Beiträge von %1$s, solange du dich nicht anmeldest. - Es liefert %1$s nicht, solange du dich nicht anmeldest. - Es nimmt deine Nachricht an %1$s nicht an, solange du dich nicht anmeldest. - Es liefert den Rest dieser Unterhaltung nicht, solange du dich nicht anmeldest. - Es liefert den Rest deiner Unterhaltung mit %1$s nicht, solange du dich nicht anmeldest. - - Trotzdem %1$d Person hinzufügen - Trotzdem %1$d Personen hinzufügen - - - %1$d Person hinzufügen - %1$d Personen hinzufügen - - - Das wäre %1$d Person. Amethyst fügt höchstens %2$d auf einmal hinzu — wähle ein paar ab. - Das wären %1$d Personen. Amethyst fügt höchstens %2$d auf einmal hinzu — wähle ein paar ab. - - - Diese Notiz wird verschlüsselt und %1$d Mal einzeln zugestellt, einmal pro Person. Mit einem externen Signierer ist das je eine Freigabe. - Diese Notiz wird verschlüsselt und %1$d Mal einzeln zugestellt, einmal pro Person. Mit einem externen Signierer ist das je eine Freigabe. - - - %1$d Person erhält eine Benachrichtigung zu diesem Beitrag. - %1$d Personen erhalten eine Benachrichtigung zu diesem Beitrag. - - - %1$s, %2$s und %3$d weitere Person - %1$s, %2$s und %3$d weitere Personen - - - %1$d Stimme ist auf einen Zeitpunkt vor der Veröffentlichung der Umfrage datiert und wird ausgeschlossen. - %1$d Stimmen sind auf einen Zeitpunkt vor der Veröffentlichung der Umfrage datiert und werden ausgeschlossen. - - - %1$d abstimmende Person wird durch deine Stummschaltliste ausgeblendet. - %1$d abstimmende Personen werden durch deine Stummschaltliste ausgeblendet. - - - %1$d Antwort hat keine gültige Option gewählt und wird ausgeschlossen. - %1$d Antworten haben keine gültige Option gewählt und werden ausgeschlossen. - - - %1$d Stimme ist nach dem Ende der Umfrage eingegangen und wird ausgeschlossen. - %1$d Stimmen sind nach dem Ende der Umfrage eingegangen und werden ausgeschlossen. - - - %1$d abstimmende Person - %1$d abstimmende Personen - - - · %1$d Auswahl - · %1$d Auswahlen - - - %1$d Stimme › - %1$d Stimmen › - - - abstimmende Person - abstimmende Personen - - - Es hält außerdem %1$d weitere Sache zurück. - Es hält außerdem %1$d weitere Dinge zurück. - - - %1$s und %2$d weitere Person - %1$s und %2$d weitere Personen - diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index ec6eb27de5..3867a441d6 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -46,6 +46,8 @@ Violência Autor desconhecido Copiar texto + Copiar original + Copiar tradução Copiar PubKey do usuário Copiar ID da Nota Copiar JSON bruto @@ -310,6 +312,18 @@ Este link de convite é inválido ou não pode ser aberto com esta conta. Não é possível abrir este link de convite. Ele pode estar desatualizado ou já ter sido substituído por um mais recente, ou ter sido criado com uma versão mais nova do app. Peça um novo link de convite. Este link de convite foi revogado e não pode mais ser usado. Peça um novo. + Esta comunidade removeu você. O link ainda funciona, mas a lista de membros dela não permite sua entrada. + Links de convite + Links de convite… + Criado %1$s + Você ainda não criou nenhum link de convite para esta comunidade. Os links criados por outros administradores são gerenciados nos dispositivos deles. + Não foi possível carregar seus links de convite, então nenhum pode ser revogado agora. Verifique sua conexão e tente novamente. + Revogar link + Revogar este link? + Quem ainda tiver este link não conseguirá mais entrar. As pessoas que já entraram com ele permanecem na comunidade. Isso não pode ser desfeito. + Revogar + Link de convite revogado. + Não foi possível revogar o link. Verifique sua conexão e tente novamente. Este link de convite expirou e não pode mais ser usado. Peça um link novo. O nome da comunidade só é revelado depois que você entra Entrar conecta aos relays deste convite, publica um anúncio de entrada assinado pela sua conta e adiciona a comunidade à sua lista. Nada é enviado até você tocar em Entrar. @@ -495,6 +509,11 @@ \n- **Mantenha** uma cópia de segurança segura de sua chave secreta para recuperação da conta. Recomendamos o uso de um gerenciador de senhas. Para segurança adicional, você pode criptografar sua chave com uma senha. Esta chave começa com **ncryptsec1** e não pode ser usada sem sua senha. \n\nSe você perder sua senha, não será possível recuperar sua chave. + Faça backup das suas chaves + Sua chave secreta é a única forma de acessar esta conta. Se você perdê-la, ela nunca poderá ser recuperada. Guarde-a em um lugar seguro agora. + Fazer backup agora + Já salvei + Dispensar Falha ao criptografar sua chave privada Chave secreta (nsec) copiada Copiar minha chave secreta @@ -1088,12 +1107,67 @@ + Entrar sem perguntar quando… + …for o meu relay, ou uma sala em que entrei + …eu estiver lendo alguém que sigo + …eu estiver enviando mensagem para alguém que sigo + …eu estiver enviando mensagem para qualquer outra pessoa + Entrar como %1$s? + pergunta quem é você + Ele não aceitará sua mensagem para %1$s a menos que você entre. + %1$s não ficará sabendo da sua resposta a menos que você entre. + Ele não entregará publicações de %1$s a menos que você entre. + Ele não aceitará sua publicação em %1$s a menos que você entre. + Ele não entregará %1$s a menos que você entre. + Ele não entregará respostas, zaps e mensagens enviadas a você a menos que você entre. + Ele não entregará o restante desta conversa a menos que você entre. + Ele não entregará o restante da sua conversa com %1$s a menos que você entre. + Este é um dos seus próprios relays, e ele pede que todos entrem. + Ele pede que todos entrem. O Amethyst não consegue saber o que ele está retendo. + alguém que você ainda não carregou + + %1$s e mais %2$d + %1$s e mais %2$d + + + Ele também está retendo mais %1$d coisa. + Ele também está retendo mais %1$d coisas. + + Lembrar para este relay + Entrar + Agora não + Nunca permitir + Como o Amethyst decide + Exceções + Nenhuma exceção ainda. Todo relay segue as regras acima. + Sempre, definido por você + Nunca, definido por você + Sempre + Nunca + Remover exceção + Exceção removida. %1$s volta a seguir suas regras. + Desfazer + Bloqueado pela sua lista de bloqueios + O Amethyst nunca entra em relays bloqueados. + Nada bloqueado. O Amethyst nunca entrará em relays que você bloquear, seja qual for a configuração aqui. + Entradas recentes + Nenhum relay pediu que você entrasse ainda. + há %1$s + suas mensagens + suas respostas + pessoas que você lê + uma sala em que você publica + uma sala que você lê + sua caixa de entrada + uma conversa + seu próprio relay + este relay Fonte: %1$s v%1$s Baixar @@ -1462,6 +1536,46 @@ Visível para Nenhum destinatário ainda: apenas você poderá ver esta nota. Adicione pessoas para compartilhá-la. + Gerenciar quem vê isto + Somente você — escolha quem mais pode ver isto + Adicione pessoas para notificar + %1$s e %2$s + %1$s · %2$s + Remover todos que foram adicionados de %1$s + Enviar de forma privada + Adicionar pessoas + Buscar pessoas e listas + Você ainda não tem listas de pessoas nem pacotes de seguidos. Use a busca acima para adicionar alguém diretamente. + %1$d · %2$d privados + Acima do limite + Já adicionado + Silenciado + Membro privado + Todos nesta nota podem ver a lista completa de destinatários. + + %1$s, %2$s e mais %3$d + %1$s, %2$s e mais %3$d + + + Esta nota será criptografada e entregue %1$d vez separada, uma por pessoa. Com um assinador externo, isso significa uma aprovação para cada. + Esta nota será criptografada e entregue %1$d vezes separadas, uma por pessoa. Com um assinador externo, isso significa uma aprovação para cada. + + + %1$d pessoa receberá uma notificação sobre esta publicação. + %1$d pessoas receberão uma notificação sobre esta publicação. + + + Isso daria %1$d pessoa. O Amethyst adiciona no máximo %2$d por vez — desmarque algumas. + Isso daria %1$d pessoas. O Amethyst adiciona no máximo %2$d por vez — desmarque algumas. + + + Adicionar %1$d pessoa + Adicionar %1$d pessoas + + + Adicionar %1$d pessoa mesmo assim + Adicionar %1$d pessoas mesmo assim + Adicionar Remover usuário das notificações Notificando. Toque para silenciar a notificação deste usuário @@ -3886,6 +4000,49 @@ Prova social Enviar Ver resultados + Resultados da enquete + Carregando enquete… + Aberta + Encerra em %1$s + Encerrada há %1$s + Todas as opções + Seu voto + + · %1$d seleção + · %1$d seleções + + Nenhum voto ainda + Verificando quantos votos existem… + %1$d de %2$d respostas carregadas de %3$d relays. + %1$d de cerca de %2$d respostas carregadas de %3$d relays. + + votante + votantes + + + %1$d voto › + %1$d votos › + + + %1$d votante + %1$d votantes + + + %1$d voto chegou depois do encerramento da enquete e foi excluído. + %1$d votos chegaram depois do encerramento da enquete e foram excluídos. + + + %1$d voto tem data anterior à publicação da enquete e foi excluído. + %1$d votos têm data anterior à publicação da enquete e foram excluídos. + + + %1$d resposta não escolheu uma opção válida e foi excluída. + %1$d respostas não escolheram uma opção válida e foram excluídas. + + + %1$d votante está oculto pela sua lista de silenciados. + %1$d votantes estão ocultos pela sua lista de silenciados. + Reiniciar Aceitar Recusar @@ -4713,161 +4870,4 @@ URL do avatar (opcional) Publicando… Publicar persona - Já adicionado - Silenciado - Membro privado - %1$d · %2$d privados - Somente você — escolha quem mais pode ver isto - Adicione pessoas para notificar - %1$s · %2$s - Remover todos que foram adicionados de %1$s - Gerenciar quem vê isto - Acima do limite - Todos nesta nota podem ver a lista completa de destinatários. - Enviar de forma privada - Você ainda não tem listas de pessoas nem pacotes de seguidos. Use a busca acima para adicionar alguém diretamente. - Buscar pessoas e listas - Adicionar pessoas - %1$s e %2$s - Já salvei - Fazer backup agora - Sua chave secreta é a única forma de acessar esta conta. Se você perdê-la, ela nunca poderá ser recuperada. Guarde-a em um lugar seguro agora. - Dispensar - Faça backup das suas chaves - Esta comunidade removeu você. O link ainda funciona, mas a lista de membros dela não permite sua entrada. - Links de convite… - Criado %1$s - Você ainda não criou nenhum link de convite para esta comunidade. Os links criados por outros administradores são gerenciados nos dispositivos deles. - Links de convite - Não foi possível carregar seus links de convite, então nenhum pode ser revogado agora. Verifique sua conexão e tente novamente. - Revogar link - Revogar - Quem ainda tiver este link não conseguirá mais entrar. As pessoas que já entraram com ele permanecem na comunidade. Isso não pode ser desfeito. - Revogar este link? - Não foi possível revogar o link. Verifique sua conexão e tente novamente. - Link de convite revogado. - Copiar original - Copiar tradução - Todas as opções - Verificando quantos votos existem… - Encerra em %1$s - Encerrada há %1$s - Carregando enquete… - Nenhum voto ainda - Aberta - %1$d de %2$d respostas carregadas de %3$d relays. - %1$d de cerca de %2$d respostas carregadas de %3$d relays. - Resultados da enquete - Seu voto - há %1$s - Entrar sem perguntar quando… - …eu estiver enviando mensagem para alguém que sigo - …eu estiver enviando mensagem para qualquer outra pessoa - …for o meu relay, ou uma sala em que entrei - …eu estiver lendo alguém que sigo - O Amethyst nunca entra em relays bloqueados. - Bloqueado pela sua lista de bloqueios - Sempre, definido por você - Nunca, definido por você - Exceção removida. %1$s volta a seguir suas regras. - Exceções - Como o Amethyst decide - Entrar - Entrar como %1$s? - Nunca permitir - Nada bloqueado. O Amethyst nunca entrará em relays que você bloquear, seja qual for a configuração aqui. - Nenhuma exceção ainda. Todo relay segue as regras acima. - Nenhum relay pediu que você entrasse ainda. - Agora não - sua caixa de entrada - seu próprio relay - suas respostas - este relay - uma sala em que você publica - pessoas que você lê - uma sala que você lê - suas mensagens - uma conversa - Entradas recentes - pergunta quem é você - Lembrar para este relay - Remover exceção - Sempre - Nunca - alguém que você ainda não carregou - Desfazer - Ele não entregará respostas, zaps e mensagens enviadas a você a menos que você entre. - Este é um dos seus próprios relays, e ele pede que todos entrem. - %1$s não ficará sabendo da sua resposta a menos que você entre. - Ele pede que todos entrem. O Amethyst não consegue saber o que ele está retendo. - Ele não aceitará sua publicação em %1$s a menos que você entre. - Ele não entregará publicações de %1$s a menos que você entre. - Ele não entregará %1$s a menos que você entre. - Ele não aceitará sua mensagem para %1$s a menos que você entre. - Ele não entregará o restante desta conversa a menos que você entre. - Ele não entregará o restante da sua conversa com %1$s a menos que você entre. - - Adicionar %1$d pessoa mesmo assim - Adicionar %1$d pessoas mesmo assim - - - Adicionar %1$d pessoa - Adicionar %1$d pessoas - - - Isso daria %1$d pessoa. O Amethyst adiciona no máximo %2$d por vez — desmarque algumas. - Isso daria %1$d pessoas. O Amethyst adiciona no máximo %2$d por vez — desmarque algumas. - - - Esta nota será criptografada e entregue %1$d vez separada, uma por pessoa. Com um assinador externo, isso significa uma aprovação para cada. - Esta nota será criptografada e entregue %1$d vezes separadas, uma por pessoa. Com um assinador externo, isso significa uma aprovação para cada. - - - %1$d pessoa receberá uma notificação sobre esta publicação. - %1$d pessoas receberão uma notificação sobre esta publicação. - - - %1$s, %2$s e mais %3$d - %1$s, %2$s e mais %3$d - - - %1$d voto tem data anterior à publicação da enquete e foi excluído. - %1$d votos têm data anterior à publicação da enquete e foram excluídos. - - - %1$d votante está oculto pela sua lista de silenciados. - %1$d votantes estão ocultos pela sua lista de silenciados. - - - %1$d resposta não escolheu uma opção válida e foi excluída. - %1$d respostas não escolheram uma opção válida e foram excluídas. - - - %1$d voto chegou depois do encerramento da enquete e foi excluído. - %1$d votos chegaram depois do encerramento da enquete e foram excluídos. - - - %1$d votante - %1$d votantes - - - · %1$d seleção - · %1$d seleções - - - %1$d voto › - %1$d votos › - - - votante - votantes - - - Ele também está retendo mais %1$d coisa. - Ele também está retendo mais %1$d coisas. - - - %1$s e mais %2$d - %1$s e mais %2$d - diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index 3f2fbae8da..d388b4abbd 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -46,6 +46,8 @@ Våld Okänd användare Kopiera Text + Kopiera originalet + Kopiera översättningen Kopiera användarens ID Kopiera anteckningens ID Kopiera rå JSON @@ -310,6 +312,18 @@ Den här inbjudningslänken är ogiltig eller kan inte öppnas med det här kontot. Den här inbjudningslänken kan inte öppnas. Den kan vara föråldrad eller redan ersatt av en nyare, eller skapad med en nyare version av appen. Be om en ny inbjudningslänk. Den här inbjudningslänken har återkallats och kan inte längre användas. Be om en ny. + Den här gemenskapen har tagit bort dig. Länken fungerar fortfarande, men dess medlemslista släpper inte in dig. + Inbjudningslänkar + Inbjudningslänkar… + Skapad %1$s + Du har inte skapat några inbjudningslänkar för den här gemenskapen än. Länkar som andra administratörer skapat hanteras på deras egna enheter. + Dina inbjudningslänkar kunde inte läsas in, så ingen kan återkallas just nu. Kontrollera din anslutning och försök igen. + Återkalla länk + Återkalla den här länken? + Den som fortfarande har den här länken kommer inte längre att kunna gå med. Personer som redan gått med via den stannar kvar i gemenskapen. Detta kan inte ångras. + Återkalla + Inbjudningslänken har återkallats. + Länken kunde inte återkallas. Kontrollera din anslutning och försök igen. Den här inbjudningslänken har upphört att gälla och kan inte längre användas. Be om en ny länk. Gemenskapens namn visas först efter att du gått med Att gå med ansluter till inbjudningens reläer, publicerar ett anslutningsmeddelande signerat av ditt konto och lägger till gemenskapen i din lista. Inget skickas förrän du trycker på Gå med. @@ -495,6 +509,11 @@ \n- **Spara** en säkerhetskopia av din hemliga nyckel för återställning av kontot. Vi rekommenderar att du använder en lösenordshanterare. För extra säkerhet kan du kryptera din nyckel med ett lösenord. Denna nyckel börjar med **ncryptsec1** och kan inte användas utan ditt lösenord. \n\nOm du tappar bort ditt lösenord kommer du inte att kunna återställa din nyckel. + Säkerhetskopiera dina nycklar + Din hemliga nyckel är enda sättet att komma åt det här kontot. Om du förlorar den kan den aldrig återskapas. Spara den på ett säkert ställe nu. + Säkerhetskopiera nu + Jag har sparat dem + Avfärda Misslyckades med att kryptera din privata nyckel Hemlig nyckel (nsec) kopierad till urklipp Kopiera min hemliga nyckel @@ -1088,12 +1107,67 @@ + Logga in utan att fråga när… + …det är mitt relä, eller ett rum jag gått med i + …jag läser någon jag följer + …jag skriver till någon jag följer + …jag skriver till någon annan + Logga in som %1$s? + frågar vem du är + Det tar inte emot ditt meddelande till %1$s om du inte loggar in. + %1$s får inte veta om ditt svar om du inte loggar in. + Det levererar inte inlägg från %1$s om du inte loggar in. + Det tar inte emot ditt inlägg i %1$s om du inte loggar in. + Det levererar inte %1$s om du inte loggar in. + Det levererar inte svar, zaps och meddelanden som skickats till dig om du inte loggar in. + Det levererar inte resten av den här konversationen om du inte loggar in. + Det levererar inte resten av din konversation med %1$s om du inte loggar in. + Det här är ett av dina egna reläer, och det ber alla att logga in. + Det ber alla att logga in. Amethyst kan inte avgöra vad det håller inne med. + någon du inte laddat in än + + %1$s och %2$d till + %1$s och %2$d till + + + Det håller också inne med %1$d annan sak. + Det håller också inne med %1$d andra saker. + + Kom ihåg för det här reläet + Logga in + Inte nu + Tillåt aldrig + Så avgör Amethyst + Undantag + Inga undantag än. Varje relä följer reglerna ovan. + Alltid, inställt av dig + Aldrig, inställt av dig + Alltid + Aldrig + Ta bort undantag + Undantaget togs bort. %1$s följer dina regler igen. + Ångra + Blockerat av din blockeringslista + Amethyst loggar aldrig in på blockerade reläer. + Inget blockerat. Reläer du blockerar loggas aldrig in på, oavsett vad du ställer in här. + Senaste inloggningar + Inget relä har bett dig logga in än. + för %1$s sedan + dina meddelanden + dina svar + personer du läser + ett rum du postar i + ett rum du läser + din inkorg + en konversation + ditt eget relä + det här reläet Källa: %1$s v%1$s Ladda ner @@ -1462,6 +1536,46 @@ Synlig för Inga mottagare ännu: bara du kommer att kunna se den här anteckningen. Lägg till personer att dela den med. + Hantera vem som ser detta + Bara du — välj vem mer som får se detta + Lägg till personer att avisera + %1$s och %2$s + %1$s · %2$s + Ta bort alla som lagts till från %1$s + Skicka privat + Lägg till personer + Sök personer och listor + Du har inga personlistor eller följpaket än. Sök ovan för att lägga till någon direkt. + %1$d · %2$d privata + Över gränsen + Redan tillagd + Tystad + Privat medlem + Alla i den här anteckningen kan se hela mottagarlistan. + + %1$s, %2$s och %3$d till + %1$s, %2$s och %3$d till + + + Den här anteckningen krypteras och levereras %1$d separat gång, en gång per person. Med en extern signerare innebär det ett godkännande var. + Den här anteckningen krypteras och levereras %1$d separata gånger, en gång per person. Med en extern signerare innebär det ett godkännande var. + + + %1$d person får en avisering om det här inlägget. + %1$d personer får en avisering om det här inlägget. + + + Det skulle bli %1$d person. Amethyst lägger till högst %2$d åt gången — avmarkera några. + Det skulle bli %1$d personer. Amethyst lägger till högst %2$d åt gången — avmarkera några. + + + Lägg till %1$d person + Lägg till %1$d personer + + + Lägg till %1$d person ändå + Lägg till %1$d personer ändå + Lägg till Ta bort användare från aviseringar Aviserar. Tryck för att tysta aviseringen för den här användaren @@ -3886,6 +4000,49 @@ Socialt bevis Skicka Visa resultat + Omröstningsresultat + Läser in omröstning… + Öppen + Stänger om %1$s + Avslutades för %1$s sedan + Alla alternativ + Din röst + + · %1$d val + · %1$d val + + Inga röster än + Kontrollerar hur många röster som finns… + %1$d av %2$d svar inlästa från %3$d reläer. + %1$d av ungefär %2$d svar inlästa från %3$d reläer. + + röstande + röstande + + + %1$d röst › + %1$d röster › + + + %1$d röstande + %1$d röstande + + + %1$d röst kom in efter att omröstningen stängde och utesluts. + %1$d röster kom in efter att omröstningen stängde och utesluts. + + + %1$d röst är daterad före omröstningen publicerades och utesluts. + %1$d röster är daterade före omröstningen publicerades och utesluts. + + + %1$d svar valde inget giltigt alternativ och utesluts. + %1$d svar valde inget giltigt alternativ och utesluts. + + + %1$d röstande döljs av din tystningslista. + %1$d röstande döljs av din tystningslista. + Starta om Acceptera Avböj @@ -4713,161 +4870,4 @@ Avatar-URL (valfritt) Publicerar… Publicera persona - Redan tillagd - Tystad - Privat medlem - %1$d · %2$d privata - Bara du — välj vem mer som får se detta - Lägg till personer att avisera - %1$s · %2$s - Ta bort alla som lagts till från %1$s - Hantera vem som ser detta - Över gränsen - Alla i den här anteckningen kan se hela mottagarlistan. - Skicka privat - Du har inga personlistor eller följpaket än. Sök ovan för att lägga till någon direkt. - Sök personer och listor - Lägg till personer - %1$s och %2$s - Jag har sparat dem - Säkerhetskopiera nu - Din hemliga nyckel är enda sättet att komma åt det här kontot. Om du förlorar den kan den aldrig återskapas. Spara den på ett säkert ställe nu. - Avfärda - Säkerhetskopiera dina nycklar - Den här gemenskapen har tagit bort dig. Länken fungerar fortfarande, men dess medlemslista släpper inte in dig. - Inbjudningslänkar… - Skapad %1$s - Du har inte skapat några inbjudningslänkar för den här gemenskapen än. Länkar som andra administratörer skapat hanteras på deras egna enheter. - Inbjudningslänkar - Dina inbjudningslänkar kunde inte läsas in, så ingen kan återkallas just nu. Kontrollera din anslutning och försök igen. - Återkalla länk - Återkalla - Den som fortfarande har den här länken kommer inte längre att kunna gå med. Personer som redan gått med via den stannar kvar i gemenskapen. Detta kan inte ångras. - Återkalla den här länken? - Länken kunde inte återkallas. Kontrollera din anslutning och försök igen. - Inbjudningslänken har återkallats. - Kopiera originalet - Kopiera översättningen - Alla alternativ - Kontrollerar hur många röster som finns… - Stänger om %1$s - Avslutades för %1$s sedan - Läser in omröstning… - Inga röster än - Öppen - %1$d av %2$d svar inlästa från %3$d reläer. - %1$d av ungefär %2$d svar inlästa från %3$d reläer. - Omröstningsresultat - Din röst - för %1$s sedan - Logga in utan att fråga när… - …jag skriver till någon jag följer - …jag skriver till någon annan - …det är mitt relä, eller ett rum jag gått med i - …jag läser någon jag följer - Amethyst loggar aldrig in på blockerade reläer. - Blockerat av din blockeringslista - Alltid, inställt av dig - Aldrig, inställt av dig - Undantaget togs bort. %1$s följer dina regler igen. - Undantag - Så avgör Amethyst - Logga in - Logga in som %1$s? - Tillåt aldrig - Inget blockerat. Reläer du blockerar loggas aldrig in på, oavsett vad du ställer in här. - Inga undantag än. Varje relä följer reglerna ovan. - Inget relä har bett dig logga in än. - Inte nu - din inkorg - ditt eget relä - dina svar - det här reläet - ett rum du postar i - personer du läser - ett rum du läser - dina meddelanden - en konversation - Senaste inloggningar - frågar vem du är - Kom ihåg för det här reläet - Ta bort undantag - Alltid - Aldrig - någon du inte laddat in än - Ångra - Det levererar inte svar, zaps och meddelanden som skickats till dig om du inte loggar in. - Det här är ett av dina egna reläer, och det ber alla att logga in. - %1$s får inte veta om ditt svar om du inte loggar in. - Det ber alla att logga in. Amethyst kan inte avgöra vad det håller inne med. - Det tar inte emot ditt inlägg i %1$s om du inte loggar in. - Det levererar inte inlägg från %1$s om du inte loggar in. - Det levererar inte %1$s om du inte loggar in. - Det tar inte emot ditt meddelande till %1$s om du inte loggar in. - Det levererar inte resten av den här konversationen om du inte loggar in. - Det levererar inte resten av din konversation med %1$s om du inte loggar in. - - Lägg till %1$d person ändå - Lägg till %1$d personer ändå - - - Lägg till %1$d person - Lägg till %1$d personer - - - Det skulle bli %1$d person. Amethyst lägger till högst %2$d åt gången — avmarkera några. - Det skulle bli %1$d personer. Amethyst lägger till högst %2$d åt gången — avmarkera några. - - - Den här anteckningen krypteras och levereras %1$d separat gång, en gång per person. Med en extern signerare innebär det ett godkännande var. - Den här anteckningen krypteras och levereras %1$d separata gånger, en gång per person. Med en extern signerare innebär det ett godkännande var. - - - %1$d person får en avisering om det här inlägget. - %1$d personer får en avisering om det här inlägget. - - - %1$s, %2$s och %3$d till - %1$s, %2$s och %3$d till - - - %1$d röst är daterad före omröstningen publicerades och utesluts. - %1$d röster är daterade före omröstningen publicerades och utesluts. - - - %1$d röstande döljs av din tystningslista. - %1$d röstande döljs av din tystningslista. - - - %1$d svar valde inget giltigt alternativ och utesluts. - %1$d svar valde inget giltigt alternativ och utesluts. - - - %1$d röst kom in efter att omröstningen stängde och utesluts. - %1$d röster kom in efter att omröstningen stängde och utesluts. - - - %1$d röstande - %1$d röstande - - - · %1$d val - · %1$d val - - - %1$d röst › - %1$d röster › - - - röstande - röstande - - - Det håller också inne med %1$d annan sak. - Det håller också inne med %1$d andra saker. - - - %1$s och %2$d till - %1$s och %2$d till - diff --git a/commons/src/commonMain/composeResources/values-cs/strings.xml b/commons/src/commonMain/composeResources/values-cs/strings.xml index 26478d17e0..33ae469b46 100644 --- a/commons/src/commonMain/composeResources/values-cs/strings.xml +++ b/commons/src/commonMain/composeResources/values-cs/strings.xml @@ -21,7 +21,41 @@ Veřejný klíč (lze sdílet): Tajný klíč (NIKDY jej nesdílejte!): Uložil jsem si klíče, pokračovat + Uložil jsem si klíče na bezpečné místo + Kopírovat zašifrovaně (doporučeno) + Heslo pro zašifrovanou zálohu + Krok %1$d z %2$d + Další + Zpět + Zrušit + Než budete pokračovat, uložte si klíče + Váš veřejný klíč (npub) je vaše sdílitelná adresa. Rozdávejte ho bez obav, aby vás lidé mohli najít a sledovat. + Váš tajný klíč (nsec) je heslo, které NIKDY nelze změnit. Toto je jediný okamžik, kdy se zobrazí — uložte si ho hned, jinak přijdete o přístup k tomuto účtu navždy. + Vaše klíče + Ještě jedna věc + Uložte si tajný klíč do správce hesel. Pokud ho ztratíte, nikdo vám ho nedokáže obnovit. + Toto je účet jen pro čtení. Nebyl vygenerován žádný tajný klíč, takže není co zálohovat. + Zálohovat klíče + Veřejný klíč (npub) + Váš veřejný klíč lze bez obav sdílet. Dejte ho lidem, aby vás mohli najít a sledovat. + Zobrazit QR kód + Skrýt QR kód + Tajný klíč (nsec) + Váš tajný klíč je jako heslo, které NIKDY nelze změnit ani obnovit. Kdokoli ho má, ovládá váš účet navždy. Nikdy ho nesdílejte; uložte si ho do správce hesel. + Tajný klíč je skrytý + Zobrazit tajný klíč + Skrýt tajný klíč + Kopírovat tajný klíč + Zkopíruje nsec v čitelné podobě. Vkládejte ho pouze do důvěryhodného správce hesel. + Kopírovat zašifrovaně (doporučeno) + Heslo pro zašifrovanou zálohu + Klíč se nepodařilo zašifrovat. Zkuste to prosím znovu. + Tento účet používá externí podepisovač — žádný tajný klíč zde není uložen. + Zkopírováno! + Kopírovat + Zobrazit tajný klíč + Pro zobrazení tajného klíče zadejte heslo zámku soukromí. Kopírovat Vložit @@ -161,38 +195,4 @@ %1$d událostí %1$d událostí - Zkopírováno! - Kopírovat - Kopírovat zašifrovaně (doporučeno) - Kopírovat tajný klíč - Zkopíruje nsec v čitelné podobě. Vkládejte ho pouze do důvěryhodného správce hesel. - Klíč se nepodařilo zašifrovat. Zkuste to prosím znovu. - Heslo pro zašifrovanou zálohu - Tento účet používá externí podepisovač — žádný tajný klíč zde není uložen. - Skrýt tajný klíč - Skrýt QR kód - Váš veřejný klíč lze bez obav sdílet. Dejte ho lidem, aby vás mohli najít a sledovat. - Veřejný klíč (npub) - Zobrazit tajný klíč - Tajný klíč je skrytý - Tajný klíč (nsec) - Váš tajný klíč je jako heslo, které NIKDY nelze změnit ani obnovit. Kdokoli ho má, ovládá váš účet navždy. Nikdy ho nesdílejte; uložte si ho do správce hesel. - Zobrazit QR kód - Zálohovat klíče - Pro zobrazení tajného klíče zadejte heslo zámku soukromí. - Zobrazit tajný klíč - Zpět - Zrušit - Uložte si tajný klíč do správce hesel. Pokud ho ztratíte, nikdo vám ho nedokáže obnovit. - Ještě jedna věc - Kopírovat zašifrovaně (doporučeno) - Heslo pro zašifrovanou zálohu - Vaše klíče - Další - Toto je účet jen pro čtení. Nebyl vygenerován žádný tajný klíč, takže není co zálohovat. - Uložil jsem si klíče na bezpečné místo - Krok %1$d z %2$d - Váš veřejný klíč (npub) je vaše sdílitelná adresa. Rozdávejte ho bez obav, aby vás lidé mohli najít a sledovat. - Váš tajný klíč (nsec) je heslo, které NIKDY nelze změnit. Toto je jediný okamžik, kdy se zobrazí — uložte si ho hned, jinak přijdete o přístup k tomuto účtu navždy. - Než budete pokračovat, uložte si klíče diff --git a/commons/src/commonMain/composeResources/values-de-rDE/strings.xml b/commons/src/commonMain/composeResources/values-de-rDE/strings.xml index 50c4d6da1e..b56087969d 100644 --- a/commons/src/commonMain/composeResources/values-de-rDE/strings.xml +++ b/commons/src/commonMain/composeResources/values-de-rDE/strings.xml @@ -21,7 +21,41 @@ Öffentlicher Schlüssel (teilbar): Geheimer Schlüssel (NIEMALS teilen!): Ich habe meine Schlüssel gespeichert, weiter + Ich habe meine Schlüssel an einem sicheren Ort gespeichert + Verschlüsselt kopieren (empfohlen) + Passwort für die verschlüsselte Sicherung + Schritt %1$d von %2$d + Weiter + Zurück + Abbrechen + Sichere deine Schlüssel, bevor du fortfährst + Dein öffentlicher Schlüssel (npub) ist deine teilbare Adresse. Gib ihn bedenkenlos weiter, damit Leute dich finden und dir folgen können. + Dein geheimer Schlüssel (nsec) ist ein Passwort, das NIE zurückgesetzt werden kann. Dies ist das einzige Mal, dass er angezeigt wird — speichere ihn jetzt, sonst verlierst du den Zugang zu diesem Konto für immer. + Deine Schlüssel + Noch eine Sache + Bewahre deinen geheimen Schlüssel in einem Passwortmanager auf. Wenn du ihn verlierst, kann ihn niemand für dich wiederherstellen. + Dies ist ein Konto mit Nur-Lese-Zugriff. Es wurde kein geheimer Schlüssel erzeugt, also gibt es nichts zu sichern. + Schlüssel sichern + Öffentlicher Schlüssel (npub) + Dein öffentlicher Schlüssel kann bedenkenlos geteilt werden. Gib ihn weiter, damit Leute dich finden und dir folgen können. + QR-Code anzeigen + QR-Code ausblenden + Geheimer Schlüssel (nsec) + Dein geheimer Schlüssel ist wie ein Passwort, das NIE zurückgesetzt oder wiederhergestellt werden kann. Wer ihn hat, kontrolliert dein Konto für immer. Teile ihn niemals; bewahre ihn in einem Passwortmanager auf. + Der geheime Schlüssel ist ausgeblendet + Geheimen Schlüssel anzeigen + Geheimen Schlüssel ausblenden + Geheimen Schlüssel kopieren + Kopiert den nsec im Klartext. Füge ihn nur in einen vertrauenswürdigen Passwortmanager ein. + Verschlüsselt kopieren (empfohlen) + Passwort für die verschlüsselte Sicherung + Der Schlüssel konnte nicht verschlüsselt werden. Bitte versuche es erneut. + Dieses Konto verwendet einen externen Signierer — hier ist kein geheimer Schlüssel gespeichert. + Kopiert! + Kopieren + Geheimen Schlüssel anzeigen + Gib dein Passwort der Privatsphäre-Sperre ein, um deinen geheimen Schlüssel anzuzeigen. Kopieren Einfügen @@ -151,38 +185,4 @@ %1$d Termin %1$d Termine - Kopiert! - Kopieren - Verschlüsselt kopieren (empfohlen) - Geheimen Schlüssel kopieren - Kopiert den nsec im Klartext. Füge ihn nur in einen vertrauenswürdigen Passwortmanager ein. - Der Schlüssel konnte nicht verschlüsselt werden. Bitte versuche es erneut. - Passwort für die verschlüsselte Sicherung - Dieses Konto verwendet einen externen Signierer — hier ist kein geheimer Schlüssel gespeichert. - Geheimen Schlüssel ausblenden - QR-Code ausblenden - Dein öffentlicher Schlüssel kann bedenkenlos geteilt werden. Gib ihn weiter, damit Leute dich finden und dir folgen können. - Öffentlicher Schlüssel (npub) - Geheimen Schlüssel anzeigen - Der geheime Schlüssel ist ausgeblendet - Geheimer Schlüssel (nsec) - Dein geheimer Schlüssel ist wie ein Passwort, das NIE zurückgesetzt oder wiederhergestellt werden kann. Wer ihn hat, kontrolliert dein Konto für immer. Teile ihn niemals; bewahre ihn in einem Passwortmanager auf. - QR-Code anzeigen - Schlüssel sichern - Gib dein Passwort der Privatsphäre-Sperre ein, um deinen geheimen Schlüssel anzuzeigen. - Geheimen Schlüssel anzeigen - Zurück - Abbrechen - Bewahre deinen geheimen Schlüssel in einem Passwortmanager auf. Wenn du ihn verlierst, kann ihn niemand für dich wiederherstellen. - Noch eine Sache - Verschlüsselt kopieren (empfohlen) - Passwort für die verschlüsselte Sicherung - Deine Schlüssel - Weiter - Dies ist ein Konto mit Nur-Lese-Zugriff. Es wurde kein geheimer Schlüssel erzeugt, also gibt es nichts zu sichern. - Ich habe meine Schlüssel an einem sicheren Ort gespeichert - Schritt %1$d von %2$d - Dein öffentlicher Schlüssel (npub) ist deine teilbare Adresse. Gib ihn bedenkenlos weiter, damit Leute dich finden und dir folgen können. - Dein geheimer Schlüssel (nsec) ist ein Passwort, das NIE zurückgesetzt werden kann. Dies ist das einzige Mal, dass er angezeigt wird — speichere ihn jetzt, sonst verlierst du den Zugang zu diesem Konto für immer. - Sichere deine Schlüssel, bevor du fortfährst diff --git a/commons/src/commonMain/composeResources/values-pt-rBR/strings.xml b/commons/src/commonMain/composeResources/values-pt-rBR/strings.xml index ce63a6344a..2a084de094 100644 --- a/commons/src/commonMain/composeResources/values-pt-rBR/strings.xml +++ b/commons/src/commonMain/composeResources/values-pt-rBR/strings.xml @@ -21,7 +21,41 @@ Chave pública (compartilhável): Chave secreta (NUNCA compartilhe isto!): Salvei minhas chaves, continuar + Guardei minhas chaves em um lugar seguro + Copiar criptografado (recomendado) + Senha para o backup criptografado + Etapa %1$d de %2$d + Próximo + Voltar + Cancelar + Salve suas chaves antes de continuar + Sua chave pública (npub) é seu endereço compartilhável. Distribua-a livremente para que as pessoas possam encontrar e seguir você. + Sua chave secreta (nsec) é uma senha que NUNCA pode ser redefinida. Esta é a única vez em que ela é exibida — salve-a agora, ou você perderá o acesso a esta conta para sempre. + Suas chaves + Uma última coisa + Guarde sua chave secreta em um gerenciador de senhas. Se você perdê-la, ninguém poderá recuperá-la para você. + Esta é uma conta somente leitura. Nenhuma chave secreta foi gerada, então não há nada para fazer backup. + Backup das chaves + Chave pública (npub) + Sua chave pública pode ser compartilhada sem risco. Passe-a para as pessoas para que possam encontrar e seguir você. + Mostrar QR code + Ocultar QR code + Chave secreta (nsec) + Sua chave secreta é como uma senha que NUNCA pode ser redefinida ou recuperada. Quem a tiver controla sua conta para sempre. Nunca a compartilhe; guarde-a em um gerenciador de senhas. + A chave secreta está oculta + Revelar chave secreta + Ocultar chave secreta + Copiar chave secreta + Copia a nsec em texto puro. Cole somente em um gerenciador de senhas confiável. + Copiar criptografado (recomendado) + Senha para o backup criptografado + Não foi possível criptografar a chave. Tente novamente. + Esta conta usa um assinador externo — nenhuma chave secreta é armazenada aqui. + Copiado! + Copiar + Revelar chave secreta + Digite a senha do bloqueio de privacidade para revelar sua chave secreta. Copiar Colar @@ -151,38 +185,4 @@ %1$d evento %1$d eventos - Copiado! - Copiar - Copiar criptografado (recomendado) - Copiar chave secreta - Copia a nsec em texto puro. Cole somente em um gerenciador de senhas confiável. - Não foi possível criptografar a chave. Tente novamente. - Senha para o backup criptografado - Esta conta usa um assinador externo — nenhuma chave secreta é armazenada aqui. - Ocultar chave secreta - Ocultar QR code - Sua chave pública pode ser compartilhada sem risco. Passe-a para as pessoas para que possam encontrar e seguir você. - Chave pública (npub) - Revelar chave secreta - A chave secreta está oculta - Chave secreta (nsec) - Sua chave secreta é como uma senha que NUNCA pode ser redefinida ou recuperada. Quem a tiver controla sua conta para sempre. Nunca a compartilhe; guarde-a em um gerenciador de senhas. - Mostrar QR code - Backup das chaves - Digite a senha do bloqueio de privacidade para revelar sua chave secreta. - Revelar chave secreta - Voltar - Cancelar - Guarde sua chave secreta em um gerenciador de senhas. Se você perdê-la, ninguém poderá recuperá-la para você. - Uma última coisa - Copiar criptografado (recomendado) - Senha para o backup criptografado - Suas chaves - Próximo - Esta é uma conta somente leitura. Nenhuma chave secreta foi gerada, então não há nada para fazer backup. - Guardei minhas chaves em um lugar seguro - Etapa %1$d de %2$d - Sua chave pública (npub) é seu endereço compartilhável. Distribua-a livremente para que as pessoas possam encontrar e seguir você. - Sua chave secreta (nsec) é uma senha que NUNCA pode ser redefinida. Esta é a única vez em que ela é exibida — salve-a agora, ou você perderá o acesso a esta conta para sempre. - Salve suas chaves antes de continuar diff --git a/commons/src/commonMain/composeResources/values-sv-rSE/strings.xml b/commons/src/commonMain/composeResources/values-sv-rSE/strings.xml index b2c73fc621..2d8dd924fa 100644 --- a/commons/src/commonMain/composeResources/values-sv-rSE/strings.xml +++ b/commons/src/commonMain/composeResources/values-sv-rSE/strings.xml @@ -21,7 +21,41 @@ Publik nyckel (delbar): Hemlig nyckel (dela ALDRIG denna!): Jag har sparat mina nycklar, fortsätt + Jag har sparat mina nycklar på ett säkert ställe + Kopiera krypterat (rekommenderas) + Lösenord för krypterad säkerhetskopia + Steg %1$d av %2$d + Nästa + Tillbaka + Avbryt + Spara dina nycklar innan du fortsätter + Din offentliga nyckel (npub) är din delbara adress. Dela ut den fritt så att andra kan hitta och följa dig. + Din hemliga nyckel (nsec) är ett lösenord som ALDRIG kan återställas. Det här är enda gången den visas — spara den nu, annars förlorar du åtkomsten till det här kontot för alltid. + Dina nycklar + En sista sak + Förvara din hemliga nyckel i en lösenordshanterare. Om du förlorar den kan ingen återskapa den åt dig. + Det här är ett skrivskyddat konto. Ingen hemlig nyckel skapades, så det finns inget att säkerhetskopiera. + Säkerhetskopiera nycklar + Offentlig nyckel (npub) + Din offentliga nyckel är trygg att dela. Ge den till andra så att de kan hitta och följa dig. + Visa QR-kod + Dölj QR-kod + Hemlig nyckel (nsec) + Din hemliga nyckel är som ett lösenord som ALDRIG kan återställas eller återskapas. Den som har den kontrollerar ditt konto för alltid. Dela den aldrig; förvara den i en lösenordshanterare. + Den hemliga nyckeln är dold + Visa hemlig nyckel + Dölj hemlig nyckel + Kopiera hemlig nyckel + Kopierar din nsec i klartext. Klistra bara in den i en betrodd lösenordshanterare. + Kopiera krypterat (rekommenderas) + Lösenord för krypterad säkerhetskopia + Nyckeln kunde inte krypteras. Försök igen. + Det här kontot använder en extern signerare — ingen hemlig nyckel lagras här. + Kopierat! + Kopiera + Visa hemlig nyckel + Ange lösenordet för integritetslåset för att visa din hemliga nyckel. Kopiera Klistra in @@ -151,38 +185,4 @@ %1$d händelse %1$d händelser - Kopierat! - Kopiera - Kopiera krypterat (rekommenderas) - Kopiera hemlig nyckel - Kopierar din nsec i klartext. Klistra bara in den i en betrodd lösenordshanterare. - Nyckeln kunde inte krypteras. Försök igen. - Lösenord för krypterad säkerhetskopia - Det här kontot använder en extern signerare — ingen hemlig nyckel lagras här. - Dölj hemlig nyckel - Dölj QR-kod - Din offentliga nyckel är trygg att dela. Ge den till andra så att de kan hitta och följa dig. - Offentlig nyckel (npub) - Visa hemlig nyckel - Den hemliga nyckeln är dold - Hemlig nyckel (nsec) - Din hemliga nyckel är som ett lösenord som ALDRIG kan återställas eller återskapas. Den som har den kontrollerar ditt konto för alltid. Dela den aldrig; förvara den i en lösenordshanterare. - Visa QR-kod - Säkerhetskopiera nycklar - Ange lösenordet för integritetslåset för att visa din hemliga nyckel. - Visa hemlig nyckel - Tillbaka - Avbryt - Förvara din hemliga nyckel i en lösenordshanterare. Om du förlorar den kan ingen återskapa den åt dig. - En sista sak - Kopiera krypterat (rekommenderas) - Lösenord för krypterad säkerhetskopia - Dina nycklar - Nästa - Det här är ett skrivskyddat konto. Ingen hemlig nyckel skapades, så det finns inget att säkerhetskopiera. - Jag har sparat mina nycklar på ett säkert ställe - Steg %1$d av %2$d - Din offentliga nyckel (npub) är din delbara adress. Dela ut den fritt så att andra kan hitta och följa dig. - Din hemliga nyckel (nsec) är ett lösenord som ALDRIG kan återställas. Det här är enda gången den visas — spara den nu, annars förlorar du åtkomsten till det här kontot för alltid. - Spara dina nycklar innan du fortsätter diff --git a/docs/changelog/translators.json b/docs/changelog/translators.json index 6364d560b3..f128485a5a 100644 --- a/docs/changelog/translators.json +++ b/docs/changelog/translators.json @@ -78,18 +78,6 @@ "tag": "v1.13.1", "since": "2026-07-28T23:17:02-04:00", "translators": [ - { - "user": "Bardesss", - "languages": [ - "Dutch" - ] - }, - { - "user": "summoner001", - "languages": [ - "Hungarian" - ] - }, { "user": "vitorpamplona", "languages": [ @@ -105,6 +93,18 @@ "Swedish" ] }, + { + "user": "Bardesss", + "languages": [ + "Dutch" + ] + }, + { + "user": "summoner001", + "languages": [ + "Hungarian" + ] + }, { "user": "maxblake2015", "languages": [