Merge remote-tracking branch 'origin/main' into claude/beautiful-brahmagupta-17clq5

# Conflicts:
#	commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/service/http/BlossomReadAuthTokenProvider.kt
This commit is contained in:
Claude
2026-09-12 21:38:48 +00:00
11 changed files with 442 additions and 19 deletions
@@ -7,7 +7,7 @@ description: Use when comparing Android strings.xml locale files to find untrans
## Overview
Extract string resource keys from a default `values/strings.xml` that are absent in a target locale's `strings.xml`, excluding non-translatable entries. Outputs missing keys and offers to translate them.
Extract string resource keys from a default `values/strings.xml` that are absent in a target locale's `strings.xml`, excluding non-translatable entries. Outputs the missing keys, then offers the two things that close them: **translate** the ones needing translation, and **copy the English value verbatim** for the ones a locale deliberately keeps in English (since 2026-09-12 that copy is what seeds Crowdin — see Background).
The repo now has **two independent Crowdin-managed resource trees** — you must scan **both** (see "Resource trees" below).
@@ -67,15 +67,23 @@ grep -nE '<string name="[^"]*">"' commonsUI/src/commonMain/composeResources/valu
**Do not** treat the value-overlap as something to deduplicate during a translation pass. Migrating amethyst's own screens onto the shared `action_*` strings is a *separate, optional* refactor and a maintainer call — out of scope for this skill. Just translate each tree correctly and independently.
## Background: Crowdin strip-identical behavior
## Background: source-identical translations and the `import_eq_suggestions` flag
This repo syncs translations via Crowdin (branch `l10n_crowdin_translations`). Crowdin's default export behavior **omits any translation that exactly equals the source**, so a key that the translator deliberately kept as English (common for brand terms like `"Nowhere Drop"`, single-word loanwords like `"Apps"` / `"Feed"` / `"Issues"`, or version prefixes like `"v%1$s"`) will not appear in the locale's `strings.xml` even though the Crowdin UI shows it as 100% translated.
This repo syncs translations via Crowdin (branch `l10n_crowdin_translations`). Crowdin does not *store* a translation that exactly equals the source unless it is told to, so historically a key a translator deliberately kept as English (brand terms like `"Nowhere Drop"`, single-word loanwords like `"Apps"` / `"Feed"` / `"Issues"`, version prefixes like `"v%1$s"`) never appeared in the locale's `strings.xml`, even though the Crowdin UI showed it as 100% translated.
**That changed on 2026-09-12.** `.github/workflows/crowdin.yml` now passes `import_eq_suggestions: true` to `crowdin/github-action`, so `upload_translations` no longer skips values equal to the source — whatever sits in the repo's locale files is seeded into Crowdin's database, identical values included. `auto_approve_imported` stays at its default `false`, so they arrive as **pending** translations for a translator to approve.
Confirmed end-to-end the same day: the first sync after the flag landed (workflow run `34706537802` → PR #4107) rewrote all five touched locale files in Crowdin's own key order with **zero net key changes** — 323 additions and 323 removals that pair up exactly. All 330 identical values pushed that morning came back down intact, unapproved included. Since Crowdin's download *replaces* file content with its export, a value it did not hold would have vanished; none did.
**Reading such a sync diff: compare key *sets* per file, never `-`/`+` lines separately.** A reorder looks identical to a mass strip under `grep '^-'`, and it will convince you the mechanism failed when nothing changed at all.
What this means for this skill:
1. **The raw on-disk diff is the candidate set.** A key missing from a locale file is either genuinely untranslated *or* a source-identical entry Crowdin stripped. Both are reported; the human decides which to skip. The Crowdin web UI ("N untranslated") is the ground truth for what genuinely needs work.
2. **Source-identical entries are a small, recognizable minority.** Brand terms (`Nowhere X`), single-word loanwords (`Apps` / `Feed` / `Issues`), and bare version/format strings (`v%1$s`) are the usual cases. Skip these by inspection rather than translating them to something identical.
3. **Don't add source-identical fallbacks.** Android falls back to `values/strings.xml` at runtime, so a key intentionally kept as English already renders correctly, and Crowdin's next sync would strip a local duplicate anyway.
1. **The raw on-disk diff is the candidate set.** A key missing from a locale file is genuinely untranslated, *or* a source-identical entry stripped before 2026-09-12 that no sync has re-seeded yet. Both are reported, and both are now actionable in the repo — translate the first, copy English into the second. The Crowdin web UI ("N untranslated") remains the ground truth for what needs human work.
2. **Source-identical entries are still recognizable, but no longer skipped.** Brand terms (`Nowhere X`), loanwords (`Apps` / `Feed` / `Issues`), symbol- or format-only values (`v%1$s`, `+%1$d`, `%1$d/%2$d`, `∞`, 👀) and example placeholders (`iPhone 13`, `https://example.com`) are the usual cases. Copy the English value into the locale file verbatim so the upload can seed it.
3. **DO add source-identical values — that is now the mechanism, not churn.** A key absent from a locale file is invisible to `upload_translations`; writing the English value in is what gets it into Crowdin, so a translator approves it once in bulk instead of typing it into the UI ~70 times per locale. (Runtime behaviour is unchanged either way: Android still falls back to `values/strings.xml`.) Two exclusions:
- **Never for `<plurals>`.** Copying English `one`/`other` into cs/pl trips `MissingQuantity`, which is a CI error (cs needs `one`/`few`/`many`/`other`). Plurals stay a Crowdin-UI job.
- **Not for words a locale would genuinely translate.** German `buzz_dm_workspace` ("Arbeitsbereich"), `workout` ("Training"), `relay_group_threads_title` ("Themen"), `calendar_rsvp_section` ("Zusagen") are *gaps*, not deliberate English keeps. Copying English there seeds a wrong pending suggestion — list those for the human to translate rather than approve.
4. **A repo-side edit to a translated value only sticks where Crowdin's database
doesn't contradict it.** Download replaces file content with Crowdin's current
@@ -96,6 +104,13 @@ What this means for this skill:
from `values/strings.xml` removes it project-wide, and attributes declared
there propagate into every export.
**This does not contradict item 3 — the two cases differ.** Seeding a key
Crowdin holds *nothing* for (the identical-value copy) sticks, because there is
no stored value to contradict it; that is exactly why the copy pass works.
*Overwriting* a value Crowdin already holds differently — including an empty
one — still loses on the next sync. Add missing entries in the repo; change
existing translations in the UI.
> **Historical note:** an earlier version of this skill tried to auto-filter the
> candidate list with a git "sync-timestamp" heuristic (skip any key added before
> the last `New Crowdin translations` commit). It was **dropped** because it
@@ -172,7 +187,7 @@ comm -23 \
This gives two lists of missing key names — keep them separate; `<plurals>` translations need the per-locale CLDR category set (see Step 5 → "Plurals: handle with care").
Crowdin can asymmetrically strip keys across locales (each translator independently chose source-identical for different keys), so **cs is not a reliable upper bound**. Diff **every** target locale and union the results — don't assume the cs set covers the others. A quick per-locale count is a useful sanity check against the Crowdin UI's "N untranslated":
Locale files are asymmetric — legacy pre-2026-09-12 strips and uneven translator progress both leave different keys missing in different locales — so **cs is not a reliable upper bound**. Diff **every** target locale and union the results — don't assume the cs set covers the others. A quick per-locale count is a useful sanity check against the Crowdin UI's "N untranslated":
```bash
for locale in cs de-rDE sv-rSE pt-rBR; do
@@ -190,7 +205,7 @@ for locale in cs de-rDE sv-rSE pt-rBR; do
done
```
The combined `strings + plurals` total should line up with the Crowdin web UI's untranslated count for that locale. If it does, the raw diff is your actionable set (minus any source-identical entries you skip by inspection — see Background).
The combined `strings + plurals` total should line up with the Crowdin web UI's untranslated count for that locale. If it does, the raw diff is your actionable set: translate what needs translating, and copy the English value verbatim for the entries a locale keeps in English (see Background).
### 3. Get English values for missing keys
@@ -458,7 +473,7 @@ When adding translated strings to locale files:
- **Append new strings at the bottom** of the file, just before the closing `</resources>` tag.
- Do NOT try to insert them in alphabetical or matching order — a separate process handles ordering.
- **Insert into each locale ONLY the keys missing from *that* locale — never a shared "union" block.** Because Crowdin strips keys asymmetrically (Step 2), a key you translate may already exist in some target locales. If you compute one union set of missing keys, translate it, and paste the *same* block into every locale, you will create **duplicate keys** in whichever locales already had them. Drive the insertion off the **per-locale** diff, not the union:
- **Insert into each locale ONLY the keys missing from *that* locale — never a shared "union" block.** Because locale files are asymmetric (Step 2), a key you translate may already exist in some target locales. If you compute one union set of missing keys, translate it, and paste the *same* block into every locale, you will create **duplicate keys** in whichever locales already had them. Drive the insertion off the **per-locale** diff, not the union:
```bash
# For each locale, insert only the keys comm -23 reports missing FOR THAT LOCALE.
@@ -535,8 +550,8 @@ When adding translated strings to locale files:
- **Forgetting `translatable="false"`** — these should never appear in locale files
- **Diffing only `<string name=`** — `<plurals>` is a separate resource type; a source `<plurals>` missing from a locale will never show up in a `<string>` diff. Always run the diff twice (once per resource type) as shown in Step 2. The same goes for `<string-array>` if the project uses it.
- **Trusting a git "sync-timestamp" heuristic to pre-filter the list** — this skill used to skip keys added before the last `New Crowdin translations` commit, on the theory that Crowdin had already "decided" them. It was dropped: a key added shortly before an export that translators hadn't reached yet is genuinely missing, so the heuristic silently dropped real work. Use the raw on-disk diff and reconcile against the Crowdin web UI's untranslated count instead.
- **Adding source-identical fallbacks locally** — they get overwritten on the next Crowdin sync. Android falls back to `values/strings.xml` at runtime anyway, so a key intentionally kept as English already renders correctly. Skip these by inspection (brand terms, loanwords, `v%1$s`-style strings); don't translate them to an identical value.
- **Skipping per-locale diffs when only diffing cs** — Crowdin can strip different keys in different locales (each translator's choice), so cs is not a reliable upper bound. Diff each target locale and union the results.
- **Skipping source-identical entries instead of copying them in** — correct before 2026-09-12, wrong now. With `import_eq_suggestions: true` the repo file is the *seed* for Crowdin's database, so a key you leave out stays untranslated in the UI forever and reappears in every future scan. Copy the English value verbatim, except for `<plurals>` (trips `MissingQuantity`) and words the locale would really translate. (Confirmed by PR #4107: 330 identical values survived the next sync with zero net changes.)
- **Skipping per-locale diffs when only diffing cs** — different keys are missing in different locales (legacy strips plus uneven translator progress), so cs is not a reliable upper bound. Diff each target locale and union the results.
- **Pasting the union set of missing keys into every locale → duplicate keys** — the union is the right set to *translate*, but the wrong set to *insert*. A key missing in only some locales, inserted into all of them, duplicates in the ones that already had it. Drive each file's insertion off its own per-locale diff (see Step 6). In `commons`, a duplicate key is build-breaking: `convertXmlValueResourcesForCommonMain` fails with `Duplicated key '…'`. **Always run the post-insertion duplicate + XML-wellformedness gate in Step 6 before declaring done.** (Happened 2026-07-21 with `ps1_save_block` / `podcast_value_for_value` / `chats_history_relays`.)
- **Declaring the pass done without running `:amethyst:lintPlayBenchmark`** — the duplicate-key + XML + `convertXmlValueResourcesForCommonMain` gate is necessary but nowhere near sufficient. `MissingQuantity` and `ImpliedQuantity` are errors, there is no lint baseline, and `abortOnError` is on, so a change that compiles and passes every check in Step 6's first half can still take CI red. Compiling is not evidence. (Happened 2026-08-13: 3 lint errors after a clean duplicate/XML gate and a green `compileFdroidDebugKotlin`.)
- **Converting a `<string>` to `<plurals>` with `other` only** — "Crowdin fills the rest" is false; `MissingQuantity` errors immediately and CI fails before any sync. Supply every category the locale uses at conversion time, and re-check the declension rather than reusing the old text for `one`.
+8
View File
@@ -31,6 +31,14 @@ jobs:
with:
upload_sources: true
upload_translations: true
# Upload translations that are identical to the English source (brand
# terms, loanwords like "Feed"/"Apps", bare formats like "v%1$s").
# Without this they are SKIPPED on upload, so a locale that deliberately
# keeps English never reaches Crowdin's DB and the key keeps coming back
# as untranslated. They arrive as normal UNAPPROVED translations --
# auto_approve_imported stays at its default false, so a translator still
# approves them in the Crowdin UI (bulk-select in the Editor).
import_eq_suggestions: true
download_translations: true
# Let the downloaded translations stay in the working tree; the single
# create-pull-request step below opens the combined PR.
@@ -2608,4 +2608,5 @@
<!-- Health Connect: activity label, shown by Health Connect next to the link into our
rationale screen. Needs to be an Android resource (not a commons Compose resource)
because android:label on the manifest entry can only reference @string/. -->
<string name="health_connect_rationale_activity_label">Health Connect a Amethyst</string>
</resources>
@@ -2390,4 +2390,5 @@
<!-- Health Connect: activity label, shown by Health Connect next to the link into our
rationale screen. Needs to be an Android resource (not a commons Compose resource)
because android:label on the manifest entry can only reference @string/. -->
<string name="health_connect_rationale_activity_label">Health Connect und Amethyst</string>
</resources>
@@ -2384,4 +2384,9 @@
<!-- Health Connect: activity label, shown by Health Connect next to the link into our
rationale screen. Needs to be an Android resource (not a commons Compose resource)
because android:label on the manifest entry can only reference @string/. -->
<string name="health_connect_rationale_activity_label">Health Connect e Amethyst</string>
<plurals name="library_directory_items">
<item quantity="one">%1$d item</item>
<item quantity="other">%1$d itens</item>
</plurals>
</resources>
@@ -2387,4 +2387,6 @@
<!-- Health Connect: activity label, shown by Health Connect next to the link into our
rationale screen. Needs to be an Android resource (not a commons Compose resource)
because android:label on the manifest entry can only reference @string/. -->
<string name="route_media">Media</string>
<string name="health_connect_rationale_activity_label">Health Connect och Amethyst</string>
</resources>
@@ -129,14 +129,11 @@ class BlossomReadAuthTokenProvider(
val fresh = CompletableDeferred<String?>()
inFlight.putIfAbsent(host, fresh)?.let { return it }
// Third look, now that this caller holds the [inFlight] slot. The second look
// above still leaves a window: a straggler can read the cache before a fast
// leader stores its token, get descheduled, and then win `putIfAbsent` only
// because that leader has since cached *and* retired its entry — and sign a
// second time. Winning the slot after the leader's `remove` means the
// leader's earlier cache write is visible here (both go through the same
// ConcurrentHashMap bin), so an entry now is the just-minted token: hand it
// out and retire the slot instead of launching a duplicate signature.
// Third look, now that this caller owns the slot. The look above still leaves a
// gap: a leader can insert, sign, cache and retire its entry entirely between
// that read and the putIfAbsent, so the map is empty again and this caller wins
// it. Any leader that retired before this insert cached first, so a token
// present now is theirs — take it and give the slot back instead of re-signing.
cachedHeader(host)?.let {
inFlight.remove(host, fresh)
fresh.complete(it)
@@ -2759,4 +2759,83 @@
<string name="buzz_persona_publishing">Publikování…</string>
<string name="buzz_persona_publish">Publikovat personu</string>
<string name="profile_card_follows_you">Sleduje vás</string>
<string name="add_hashtag_label_field">Hashtag</string>
<string name="ai_tone_emojify">+ Emoji</string>
<string name="app_definition_nip">NIP-%1$s</string>
<string name="buzz_invite_dismiss">OK</string>
<string name="buzz_invite_role">Role</string>
<string name="buzz_system_unknown">%1$s: %2$s</string>
<string name="buzz_workflow_id_prefix">Workflow: %1$s</string>
<string name="buzz_workflow_picker_label">Workflow</string>
<string name="cashu_mint_label">Mint: %1$s</string>
<string name="cashu_mint_reachable_named">✓ %1$s</string>
<string name="classifieds_title_placeholder">iPhone 13</string>
<string name="dm_sender_reported_more_count">+%1$d</string>
<string name="dvm_offline">Offline</string>
<string name="error_dialog_button_ok">OK</string>
<string name="event_sync_less_than_until">&lt;%1$s</string>
<string name="gif">Gif</string>
<string name="git_commit">Commit</string>
<string name="git_repo_plain_text">Text</string>
<string name="goal_amount_placeholder">100000</string>
<string name="goal_image_placeholder">https://example.com/image.jpg</string>
<string name="goal_website_placeholder">https://example.com</string>
<string name="hls_codec_h264">H.264</string>
<string name="language_preference_pair">%1$s → %2$s</string>
<string name="live_stream_offline_tag">OFFLINE</string>
<string name="marmot_avatar_url_placeholder">https://example.com/avatar.png</string>
<string name="marmot_user_fallback_name">%1$s…</string>
<string name="my_awesome_name">Ostrich McAwesome</string>
<string name="nest_tab_chat">Chat</string>
<string name="nip46_signer_act_other">%1$s</string>
<string name="nip46_signer_act_ping">Ping</string>
<string name="nip82_version_label">v%1$s</string>
<string name="not_available_acronym">N/A</string>
<string name="nutzap">Nutzap</string>
<string name="onchain_send_fee_rate_eta">%1$s sat/vB · %2$s</string>
<string name="onchain_send_sats_amount">%1$s sats</string>
<string name="onchain_send_sats_suffix">sats</string>
<string name="platform_android">Android</string>
<string name="platform_ios">iOS</string>
<string name="platform_web">Web</string>
<string name="podcast_episode_number">Ep %1$d</string>
<string name="podcast_role_editor">Editor</string>
<string name="podcast_season_episode">S%1$d · E%2$d</string>
<string name="podcast_value_stream_rate">%1$d sats/min</string>
<string name="podcast_video">Video</string>
<string name="profile_card_bot">Bot</string>
<string name="reactions_settings_boost">Boost</string>
<string name="reactions_settings_zap">Zap</string>
<string name="relay_filter_limit">limit %1$d</string>
<string name="relay_group_message_count_short_capped">%1$d+</string>
<string name="reload_mint_sats_amount">%1$s sats</string>
<string name="secret_visible_text_placeholder">😎</string>
<string name="security_unlimited"></string>
<string name="send_payment_method_cashu">Cashu</string>
<string name="send_payment_method_onchain">On-chain</string>
<string name="share_of">%1$d/%2$d</string>
<string name="tip">Tip</string>
<string name="video_quality_auto">Auto</string>
<string name="wallet_sats">sats</string>
<string name="web_bookmark_tags_placeholder">nostr, tech, blog</string>
<string name="web_bookmark_url_label">URL</string>
<string name="web_bookmark_url_placeholder">https://example.com</string>
<string name="workout_suggestion_distance_km">%1$s km</string>
<string name="health_connect_rationale_headline">Amethyst čte dokončené tréninky, aby za vás mohl předvyplnit příspěvek o tréninku.</string>
<string name="health_connect_rationale_title">Health Connect a Amethyst</string>
<string name="health_connect_rationale_intro">Amethyst je sociální klient sítě Nostr. Jeho sekce Tréninky vám umožňuje zveřejnit souhrn dokončeného tréninku na relaye Nostr, které si zvolíte, aby lidé, kteří vás sledují, viděli, co jste dělali. Místo ručního vypisování každého čísla může Amethyst načíst trénink, který vaše hodinky nebo fitness aplikace už uložily do Health Connect, a příspěvek předvyplnit. Předvyplněný příspěvek vždy uvidíte a sami rozhodnete, zda ho zveřejníte.</string>
<string name="health_connect_rationale_what_title">Co Amethyst čte a proč</string>
<string name="health_connect_rationale_exercise">Cvičení · jakou aktivitu jste dělali, kdy začala a jak dlouho trvala — to je samotný trénink a zároveň název a doba trvání příspěvku.</string>
<string name="health_connect_rationale_steps">Kroky · počet kroků při běhu, chůzi nebo turistice.</string>
<string name="health_connect_rationale_distance">Vzdálenost · jak daleko jste se dostali, zobrazená jako vzdálenost běhu, jízdy, chůze nebo plavání.</string>
<string name="health_connect_rationale_calories">Aktivní a celkové kalorie · energie spálená při tréninku. Aktivní kalorie se použijí, pokud je váš zdroj zaznamenává; celkové kalorie jsou náhradou pro zdroje, které zaznamenávají pouze celkovou energii.</string>
<string name="health_connect_rationale_elevation">Převýšení · kolik jste nastoupali, což odlišuje rovinatou jízdu od kopcovité.</string>
<string name="health_connect_rationale_heart_rate">Tepová frekvence · průměrná a maximální tepová frekvence během tréninku, běžné měřítko náročnosti.</string>
<string name="health_connect_rationale_limits_title">Co Amethyst nedělá</string>
<string name="health_connect_rationale_limit_window">Čte pouze tréninky dokončené za posledních 7 dní, a to jen když je otevřený editor Tréninků. Na pozadí nečte nikdy.</string>
<string name="health_connect_rationale_limit_publish">Nic neopustí váš telefon, dokud sami neklepnete na návrh a příspěvek nezveřejníte. Amethyst nemá žádný server: příspěvek jde na relaye Nostr, které jste nastavili.</string>
<string name="health_connect_rationale_limit_write">Nikdy nic nezapisuje do Health Connect a nikdy nežádá o trasu vašeho cvičení, polohu ani jiný typ zdravotních údajů.</string>
<string name="health_connect_rationale_limit_optional">Celá funkce je volitelná. Vypnete ji v Nastavení → Nastavení editoru, nebo kdykoli odeberete oprávnění v Health Connect — zbytek Amethystu funguje dál.</string>
<string name="health_connect_rationale_privacy_policy">Přečíst si celé zásady ochrany soukromí</string>
<string name="workout_suggestion_connect_details">Co Amethyst čte</string>
</resources>
@@ -2697,4 +2697,131 @@
<string name="buzz_persona_publishing">Wird veröffentlicht…</string>
<string name="buzz_persona_publish">Persona veröffentlichen</string>
<string name="profile_card_follows_you">Folgt dir</string>
<string name="add_hashtag_label_field">Hashtag</string>
<string name="ai_tone_emojify">+ Emoji</string>
<string name="app_definition_kind_app">App</string>
<string name="app_definition_nip">NIP-%1$s</string>
<string name="badge_name_label">Name</string>
<string name="buzz_canvas_body_label">Canvas (Markdown)</string>
<string name="buzz_canvas_title">Canvas</string>
<string name="buzz_dm_workspace">Workspace</string>
<string name="buzz_invite_dismiss">OK</string>
<string name="buzz_invite_workspace">Workspace</string>
<string name="buzz_job_board_title">Backlog</string>
<string name="buzz_system_unknown">%1$s: %2$s</string>
<string name="buzz_workflow_def_name">Name</string>
<string name="buzz_workflow_id_prefix">Workflow: %1$s</string>
<string name="buzz_workflow_picker_label">Workflow</string>
<string name="calendar_rsvp_section">RSVPs (%1$d)</string>
<string name="cashu_mint_reachable_named">✓ %1$s</string>
<string name="cashu_mints">Mints</string>
<string name="cashu_wizard_mints_label">Mints: %1$s</string>
<string name="chat_minichat_title">Thread</string>
<string name="classifieds_title_placeholder">iPhone 13</string>
<string name="clink_budget_set">Budget</string>
<string name="concord_create_name">Name</string>
<string name="concord_view_inline">Inline</string>
<string name="dm_sender_reported_more_count">+%1$d</string>
<string name="dvm_offline">Offline</string>
<string name="error_dialog_button_ok">OK</string>
<string name="event_sync_dm_relays">DMs</string>
<string name="event_sync_inbox_relays">Inbox</string>
<string name="event_sync_less_than_until">&lt;%1$s</string>
<string name="event_sync_outbox_relays">Outbox</string>
<string name="event_sync_relays_progress">Relays: %1$d / %2$d</string>
<string name="feed">Feed</string>
<string name="fork">Fork</string>
<string name="gif">Gif</string>
<string name="git_branch">Branch</string>
<string name="git_commit">Commit</string>
<string name="git_repo_branches">Branches</string>
<string name="git_repo_commits">Commits</string>
<string name="git_repo_default_branch">default</string>
<string name="git_repo_plain_text">Text</string>
<string name="git_repo_settings_name">Name</string>
<string name="git_repo_stat_branches">Branches</string>
<string name="git_repo_stat_tags">Tags</string>
<string name="git_repo_tab_code">Code</string>
<string name="git_repo_tags">Tags</string>
<string name="goal_amount_placeholder">100000</string>
<string name="goal_image_placeholder">https://example.com/image.jpg</string>
<string name="hls_codec_h264">H.264</string>
<string name="hls_codec_label">Codec</string>
<string name="language_preference_pair">%1$s → %2$s</string>
<string name="live_stream_live_tag">LIVE</string>
<string name="live_stream_offline_tag">OFFLINE</string>
<string name="malware">Malware</string>
<string name="marmot_avatar_url_placeholder">https://example.com/avatar.png</string>
<string name="marmot_relays_header">Relays</string>
<string name="marmot_user_fallback_name">%1$s…</string>
<string name="music_track_album_label">Album (optional)</string>
<string name="my_awesome_name">Ostrich McAwesome</string>
<string name="nest_live_chip">LIVE</string>
<string name="nest_role_moderator">Moderator</string>
<string name="nest_tab_chat">Chat</string>
<string name="nests_servers_relay_label">Relay</string>
<string name="nip46_signer_act_other">%1$s</string>
<string name="nip46_signer_act_ping">Ping</string>
<string name="nip46_signer_live">Live</string>
<string name="nip82_version_label">v%1$s</string>
<string name="not_available_acronym">N/A</string>
<string name="nutzap">Nutzap</string>
<string name="onchain_send_fee_rate_eta">%1$s sat/vB · %2$s</string>
<string name="onchain_send_sats_amount">%1$s sats</string>
<string name="onchain_send_sats_suffix">sats</string>
<string name="platform_android">Android</string>
<string name="platform_ios">iOS</string>
<string name="platform_web">Web</string>
<string name="podcast_bookmarks">Podcasts</string>
<string name="podcast_episode_audio_url_placeholder">https://…/episode.mp3</string>
<string name="podcast_season_episode">S%1$d · E%2$d</string>
<string name="podcast_trailer">Trailer</string>
<string name="podcast_value_recipient_name">Name (optional)</string>
<string name="podcast_video">Video</string>
<string name="post_not_found_short">👀</string>
<string name="profile_apps_header">Apps · %1$d</string>
<string name="profile_apps_header_empty">Apps</string>
<string name="profile_card_bot">Bot</string>
<string name="reactions_settings_boost">Boost</string>
<string name="reactions_settings_zap">Zap</string>
<string name="relay_group_badge_live">LIVE</string>
<string name="relay_group_field_name">Name</string>
<string name="relay_group_message_count_short_capped">%1$d+</string>
<string name="relay_group_role_moderator">Moderator</string>
<string name="relay_group_threads_title">Threads</string>
<string name="relay_group_view_inline">Inline</string>
<string name="relays"> Relays</string>
<string name="search_source_relays">Relays</string>
<string name="secret_visible_text_placeholder">😎</string>
<string name="security_unlimited"></string>
<string name="send_payment_method_cashu">Cashu</string>
<string name="send_payment_method_lightning">Lightning</string>
<string name="share_of">%1$d/%2$d</string>
<string name="tags_label"># Tags</string>
<string name="version">Version</string>
<string name="version_name">Version %1$s</string>
<string name="video_quality_auto">Auto</string>
<string name="wallet_filter_zaps">Zaps</string>
<string name="web_bookmark_tags_placeholder">nostr, tech, blog</string>
<string name="web_bookmark_url_label">URL</string>
<string name="website">Website</string>
<string name="workout">Workout</string>
<string name="workout_suggestion_distance_km">%1$s km</string>
<string name="health_connect_rationale_headline">Amethyst liest abgeschlossene Workouts, um einen Workout-Beitrag für dich vorauszufüllen.</string>
<string name="health_connect_rationale_title">Health Connect und Amethyst</string>
<string name="health_connect_rationale_intro">Amethyst ist ein sozialer Nostr-Client. Im Bereich Workouts kannst du eine Zusammenfassung eines abgeschlossenen Workouts an die Nostr-Relays deiner Wahl veröffentlichen, damit die Leute, die dir folgen, sehen können, was du gemacht hast. Statt jede Zahl von Hand einzutippen, kann Amethyst das Workout lesen, das deine Uhr oder Fitness-App bereits in Health Connect gespeichert hat, und den Beitrag vorausfüllen. Du siehst den vorausgefüllten Beitrag immer und entscheidest, ob du ihn veröffentlichst.</string>
<string name="health_connect_rationale_what_title">Was Amethyst liest und warum</string>
<string name="health_connect_rationale_exercise">Übung · welche Aktivität du gemacht hast, wann sie begann und wie lange sie dauerte — das ist das Workout selbst sowie Titel und Dauer des Beitrags.</string>
<string name="health_connect_rationale_steps">Schritte · die Schrittzahl eines Laufs, Spaziergangs oder einer Wanderung.</string>
<string name="health_connect_rationale_distance">Distanz · wie weit du gekommen bist, angezeigt als Distanz des Laufs, der Fahrt, des Spaziergangs oder des Schwimmens.</string>
<string name="health_connect_rationale_calories">Aktive und gesamte Kalorien · die beim Workout verbrannte Energie. Aktive Kalorien werden verwendet, wenn deine Quelle sie aufzeichnet; gesamte Kalorien sind der Ersatz für Quellen, die nur die Gesamtenergie aufzeichnen.</string>
<string name="health_connect_rationale_elevation">Höhenmeter · wie viel du gestiegen bist, was eine flache Fahrt von einer hügeligen unterscheidet.</string>
<string name="health_connect_rationale_heart_rate">Herzfrequenz · die durchschnittliche und maximale Herzfrequenz während des Workouts, das übliche Maß für die Anstrengung.</string>
<string name="health_connect_rationale_limits_title">Was Amethyst nicht tut</string>
<string name="health_connect_rationale_limit_window">Liest nur Workouts, die in den letzten 7 Tagen beendet wurden, und nur während der Workout-Editor geöffnet ist. Im Hintergrund wird nie gelesen.</string>
<string name="health_connect_rationale_limit_publish">Nichts verlässt dein Telefon, bis du auf einen Vorschlag tippst und den Beitrag selbst veröffentlichst. Amethyst hat keinen Server: Der Beitrag geht an die Nostr-Relays, die du eingerichtet hast.</string>
<string name="health_connect_rationale_limit_write">Schreibt nie etwas in Health Connect und fragt nie nach deiner Trainingsroute, deinem Standort oder anderen Gesundheitsdaten.</string>
<string name="health_connect_rationale_limit_optional">Die gesamte Funktion ist optional. Schalte sie unter Einstellungen → Editor-Einstellungen aus oder entziehe die Berechtigungen jederzeit in Health Connect — der Rest von Amethyst funktioniert weiter.</string>
<string name="health_connect_rationale_privacy_policy">Vollständige Datenschutzerklärung lesen</string>
<string name="workout_suggestion_connect_details">Was Amethyst liest</string>
</resources>
@@ -2729,4 +2729,99 @@
<string name="buzz_persona_publishing">Publicando…</string>
<string name="buzz_persona_publish">Publicar persona</string>
<string name="profile_card_follows_you">Segue você</string>
<string name="add_hashtag_label_field">Hashtag</string>
<string name="ai_tone_emojify">+ Emoji</string>
<string name="amount_in_bits">%1$s bits</string>
<string name="app_definition_nip">NIP-%1$s</string>
<string name="buzz_canvas_body_label">Canvas (Markdown)</string>
<string name="buzz_canvas_title">Canvas</string>
<string name="buzz_invite_dismiss">OK</string>
<string name="buzz_job_board_title">Backlog</string>
<string name="buzz_system_unknown">%1$s: %2$s</string>
<string name="cashu_mint_label">Mint: %1$s</string>
<string name="cashu_mint_reachable_named">✓ %1$s</string>
<string name="cashu_mints">Mints</string>
<string name="cashu_wizard_mints_label">Mints: %1$s</string>
<string name="classifieds_title_placeholder">iPhone 13</string>
<string name="dm_sender_reported_more_count">+%1$d</string>
<string name="dvm_offline">Offline</string>
<string name="emoji_pack_count">%1$d emojis</string>
<string name="error_dialog_button_ok">OK</string>
<string name="event_sync_dm_relays">DMs</string>
<string name="event_sync_less_than_until">&lt;%1$s</string>
<string name="feed">Feed</string>
<string name="gif">Gif</string>
<string name="git_branch">Branch</string>
<string name="git_commit">Commit</string>
<string name="git_repo_branches">Branches</string>
<string name="git_repo_commits">Commits</string>
<string name="git_repo_stat_branches">Branches</string>
<string name="git_repo_stat_tags">Tags</string>
<string name="git_repo_tags">Tags</string>
<string name="goal_amount_placeholder">100000</string>
<string name="hls_codec_h264">H.264</string>
<string name="hls_codec_label">Codec</string>
<string name="interest_set_hashtag_count">%1$d hashtag(s)</string>
<string name="language_preference_pair">%1$s → %2$s</string>
<string name="malware">Malware</string>
<string name="marmot_avatar_url_placeholder">https://example.com/avatar.png</string>
<string name="marmot_user_fallback_name">%1$s…</string>
<string name="nip46_signer_act_other">%1$s</string>
<string name="nip46_signer_act_ping">Ping</string>
<string name="nip82_section_links">Links</string>
<string name="nip82_version_label">v%1$s</string>
<string name="not_available_acronym">N/A</string>
<string name="nutzap">Nutzap</string>
<string name="onchain_send_fee_rate_eta">%1$s sat/vB · %2$s</string>
<string name="onchain_send_sats_amount">%1$s sats</string>
<string name="onchain_send_sats_suffix">sats</string>
<string name="original">original</string>
<string name="picture_in_picture">Picture-in-Picture</string>
<string name="platform_android">Android</string>
<string name="platform_ios">iOS</string>
<string name="platform_web">Web</string>
<string name="podcast_bookmarks">Podcasts</string>
<string name="podcast_role_editor">Editor</string>
<string name="podcast_trailer">Trailer</string>
<string name="podcast_value_stream_rate">%1$d sats/min</string>
<string name="post_not_found_short">👀</string>
<string name="profile_apps_header">Apps · %1$d</string>
<string name="profile_card_bot">Bot</string>
<string name="reactions_settings_boost">Boost</string>
<string name="reactions_settings_zap">Zap</string>
<string name="relay_group_message_count_short_capped">%1$d+</string>
<string name="reload_mint_sats_amount">%1$s sats</string>
<string name="search_source_local">Local</string>
<string name="secret_visible_text_placeholder">😎</string>
<string name="security_unlimited"></string>
<string name="send_payment_method_cashu">Cashu</string>
<string name="send_payment_method_lightning">Lightning</string>
<string name="send_payment_method_onchain">On-chain</string>
<string name="share_of">%1$d/%2$d</string>
<string name="tags_label"># Tags</string>
<string name="video_player_settings_action_pip">Picture-in-Picture</string>
<string name="video_quality_auto">Auto</string>
<string name="wallet_filter_zaps">Zaps</string>
<string name="wallet_sats">sats</string>
<string name="web_bookmark_tags_placeholder">nostr, tech, blog</string>
<string name="web_bookmark_url_label">URL</string>
<string name="workout_suggestion_distance_km">%1$s km</string>
<string name="workout_volume">Volume</string>
<string name="health_connect_rationale_headline">O Amethyst lê treinos concluídos para preencher previamente uma publicação de treino para você.</string>
<string name="health_connect_rationale_title">Health Connect e Amethyst</string>
<string name="health_connect_rationale_intro">O Amethyst é um cliente social do Nostr. A seção Treinos permite publicar um resumo de um treino concluído nos relays do Nostr que você escolher, para que quem segue você veja o que você fez. Em vez de digitar cada número à mão, o Amethyst pode ler o treino que seu relógio ou aplicativo de fitness já salvou no Health Connect e preencher a publicação previamente. Você sempre vê a publicação preenchida e decide se quer publicá-la.</string>
<string name="health_connect_rationale_what_title">O que o Amethyst lê e por quê</string>
<string name="health_connect_rationale_exercise">Exercício · qual atividade você fez, quando começou e quanto tempo durou — é o treino em si, além do título e da duração da publicação.</string>
<string name="health_connect_rationale_steps">Passos · a contagem de passos de uma corrida, caminhada ou trilha.</string>
<string name="health_connect_rationale_distance">Distância · o quanto você percorreu, exibido como a distância da corrida, pedalada, caminhada ou natação.</string>
<string name="health_connect_rationale_calories">Calorias ativas e totais · a energia gasta no treino. As calorias ativas são usadas quando sua fonte as registra; as calorias totais são a alternativa para fontes que registram apenas a energia total.</string>
<string name="health_connect_rationale_elevation">Elevação · o quanto você subiu, que é o que distingue um percurso plano de um acidentado.</string>
<string name="health_connect_rationale_heart_rate">Frequência cardíaca · a frequência média e máxima durante o treino, a medida padrão do esforço.</string>
<string name="health_connect_rationale_limits_title">O que o Amethyst não faz</string>
<string name="health_connect_rationale_limit_window">Lê apenas treinos concluídos nos últimos 7 dias e somente enquanto o editor de Treinos está aberto. Nunca lê em segundo plano.</string>
<string name="health_connect_rationale_limit_publish">Nada sai do seu telefone até você tocar em uma sugestão e publicar por conta própria. O Amethyst não tem servidor: a publicação vai para os relays do Nostr que você configurou.</string>
<string name="health_connect_rationale_limit_write">Nunca grava nada no Health Connect e nunca solicita seu trajeto de exercício, localização ou qualquer outro tipo de dado de saúde.</string>
<string name="health_connect_rationale_limit_optional">Todo o recurso é opcional. Desative-o em Configurações → Configurações de composição, ou revogue as permissões no Health Connect a qualquer momento — o restante do Amethyst continua funcionando.</string>
<string name="health_connect_rationale_privacy_policy">Ler a política de privacidade completa</string>
<string name="workout_suggestion_connect_details">O que o Amethyst lê</string>
</resources>
@@ -2731,4 +2731,97 @@
<string name="buzz_persona_publishing">Publicerar…</string>
<string name="buzz_persona_publish">Publicera persona</string>
<string name="profile_card_follows_you">Följer dig</string>
<string name="add_hashtag_label_field">Hashtag</string>
<string name="ai_tone_emojify">+ Emoji</string>
<string name="app_definition_kind_app">App</string>
<string name="app_definition_nip">NIP-%1$s</string>
<string name="app_definition_via">via %1$s</string>
<string name="banner_url">Banner URL</string>
<string name="buzz_canvas_body_label">Canvas (Markdown)</string>
<string name="buzz_canvas_title">Canvas</string>
<string name="buzz_invite_dismiss">OK</string>
<string name="buzz_system_unknown">%1$s: %2$s</string>
<string name="cashu_mint_label">Mint: %1$s</string>
<string name="cashu_mint_reachable_named">✓ %1$s</string>
<string name="cashu_mints">Mints</string>
<string name="cashu_wizard_mints_label">Mints: %1$s</string>
<string name="classifieds_title_placeholder">iPhone 13</string>
<string name="clink_budget_set">Budget</string>
<string name="dm_sender_reported_more_count">+%1$d</string>
<string name="dvm_offline">Offline</string>
<string name="emoji_pack_count">%1$d emojis</string>
<string name="event_sync_less_than_until">&lt;%1$s</string>
<string name="gif">Gif</string>
<string name="git_commit">Commit</string>
<string name="git_repo_commits">Commits</string>
<string name="git_repo_plain_text">Text</string>
<string name="goal_amount_placeholder">100000</string>
<string name="goal_image_placeholder">https://example.com/image.jpg</string>
<string name="hls_codec_h264">H.264</string>
<string name="hls_codec_label">Codec</string>
<string name="language_preference_pair">%1$s → %2$s</string>
<string name="live_stream_live_tag">LIVE</string>
<string name="live_stream_offline_tag">OFFLINE</string>
<string name="marmot_avatar_url_placeholder">https://example.com/avatar.png</string>
<string name="marmot_user_fallback_name">%1$s…</string>
<string name="music_track_artist_label">Artist</string>
<string name="nest_live_chip">LIVE</string>
<string name="nest_role_moderator">Moderator</string>
<string name="nip46_signer_act_other">%1$s</string>
<string name="nip46_signer_act_ping">Ping</string>
<string name="nip46_signer_live">Live</string>
<string name="nip82_version_label">v%1$s</string>
<string name="not_available_acronym">N/A</string>
<string name="nutzap">Nutzap</string>
<string name="onchain_send_fee_rate_eta">%1$s sat/vB · %2$s</string>
<string name="onchain_send_sats_amount">%1$s sats</string>
<string name="onchain_send_sats_suffix">sats</string>
<string name="original">original</string>
<string name="platform_android">Android</string>
<string name="platform_ios">iOS</string>
<string name="platform_web">Web</string>
<string name="podcast_explicit">Explicit</string>
<string name="podcast_trailer">Trailer</string>
<string name="podcast_value_node_pubkey_hint">02abc… (33-byte hex)</string>
<string name="podcast_value_stream_rate">%1$d sats/min</string>
<string name="podcast_video">Video</string>
<string name="post_not_found_short">👀</string>
<string name="profile_card_bot">Bot</string>
<string name="reactions_settings_zap">Zap</string>
<string name="relay_group_badge_live">LIVE</string>
<string name="relay_group_message_count_short_capped">%1$d+</string>
<string name="relay_group_role_moderator">Moderator</string>
<string name="reload_mint_sats_amount">%1$s sats</string>
<string name="secret_visible_text_placeholder">😎</string>
<string name="security_unlimited"></string>
<string name="send_payment_method_cashu">Cashu</string>
<string name="send_payment_method_lightning">Lightning</string>
<string name="send_payment_method_onchain">On-chain</string>
<string name="share_of">%1$d/%2$d</string>
<string name="version">Version</string>
<string name="version_name">Version %1$s</string>
<string name="video_quality_auto">Auto</string>
<string name="wallet_add_clink_title">CLINK Debit</string>
<string name="wallet_filter_zaps">Zaps</string>
<string name="wallet_sats">sats</string>
<string name="web_bookmark_tags_placeholder">nostr, tech, blog</string>
<string name="web_bookmark_url_label">URL</string>
<string name="workout_suggestion_distance_km">%1$s km</string>
<string name="health_connect_rationale_headline">Amethyst läser avslutade träningspass för att kunna förifylla ett träningsinlägg åt dig.</string>
<string name="health_connect_rationale_title">Health Connect och Amethyst</string>
<string name="health_connect_rationale_intro">Amethyst är en social Nostr-klient. I avsnittet Träningspass kan du publicera en sammanfattning av ett avslutat träningspass till de Nostr-reläer du väljer, så att de som följer dig kan se vad du har gjort. I stället för att skriva in varje siffra för hand kan Amethyst läsa det träningspass som din klocka eller träningsapp redan har sparat i Health Connect och förifylla inlägget. Du ser alltid det förifyllda inlägget och avgör själv om det ska publiceras.</string>
<string name="health_connect_rationale_what_title">Vad Amethyst läser och varför</string>
<string name="health_connect_rationale_exercise">Övning · vilken aktivitet du gjorde, när den började och hur länge den varade — det är själva träningspasset och även inläggets titel och varaktighet.</string>
<string name="health_connect_rationale_steps">Steg · antalet steg under en löprunda, promenad eller vandring.</string>
<string name="health_connect_rationale_distance">Distans · hur långt du tog dig, som visas som distansen för löprundan, cykelturen, promenaden eller simningen.</string>
<string name="health_connect_rationale_calories">Aktiva och totala kalorier · energin som träningspasset förbrände. Aktiva kalorier används när din källa registrerar dem; totala kalorier är reserven för källor som bara registrerar total energi.</string>
<string name="health_connect_rationale_elevation">Höjdmeter · hur mycket du klättrade, vilket skiljer en platt tur från en kuperad.</string>
<string name="health_connect_rationale_heart_rate">Puls · genomsnittlig och maximal puls under träningspasset, det vanliga måttet på hur ansträngande det var.</string>
<string name="health_connect_rationale_limits_title">Vad Amethyst inte gör</string>
<string name="health_connect_rationale_limit_window">Läser bara träningspass som avslutades de senaste 7 dagarna, och bara medan träningsredigeraren är öppen. Den läser aldrig i bakgrunden.</string>
<string name="health_connect_rationale_limit_publish">Ingenting lämnar din telefon förrän du trycker på ett förslag och publicerar inlägget själv. Amethyst har ingen server: inlägget går till de Nostr-reläer du har ställt in.</string>
<string name="health_connect_rationale_limit_write">Skriver aldrig något till Health Connect och begär aldrig din träningsrutt, plats eller någon annan typ av hälsodata.</string>
<string name="health_connect_rationale_limit_optional">Hela funktionen är valfri. Stäng av den under Inställningar → Skrivinställningar, eller återkalla behörigheterna i Health Connect när som helst — resten av Amethyst fortsätter att fungera.</string>
<string name="health_connect_rationale_privacy_policy">Läs hela integritetspolicyn</string>
<string name="workout_suggestion_connect_details">Vad Amethyst läser</string>
</resources>