mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-14 14:19:57 +02:00
Creating a targeted transfer contacts the peer and only returns once the offer is answered or its timeouts expire — connection_timeout plus offer_wait_timeout, so minutes against a device that never answers. The composer disabled every control for that whole window, including Close, leaving no way out. Worse, the core records the row before it reaches out and leaves it in `failed` when the peer never replies, so giving up still produced a failure notification and a history entry for a send the user had already called off. Cancelling now reaches the core while that create is still running. The `created` lifecycle event carries the transfer id and is emitted before the peer is contacted, so the id is known in time; the cancel goes out through the interrupt lane, which exists precisely to reach a core busy inside another call. The transfer is then deleted, and its id is filtered out of the published list so a refresh racing the delete cannot leak it into history or into a notification. If the id has not arrived yet, the result carries it and the same cleanup runs on return. Picked sources are released only once the call lands, because the import owns them until then, and a generation counter keeps a late result from disturbing a newer send. Close and Cancel were also the same action under two labels. There is now one control: the sheet's cancellation item, reading Cancel while a send is waiting and Close otherwise, never disabled. Two further fixes from device testing: - Receive and Resume were gated on transfer state alone, so an approved outgoing transfer offered the sender a Receive button — an invitation to download the files it was uploading. Both pull into a local folder and are now receiver-only. - Renamed the deprecated `laptopcomputerAndIphone` symbol to `macbookAndIphone`; the deployment targets are well past where it was introduced, so no availability guard is needed. Adds a typed `targetedTransferId` accessor on CoreEventModel. This is a narrow, deliberate exception to the wake-up-only event rule in DESIGN-DEVICE-HISTORY.md §13: it takes the subject id and never state, and it exists because no query can answer while the create holds the serial lane — which is exactly when the user wants to cancel. Known gap: direction is inferred by comparing endpoint ids, because the binding does not expose the row's role. After an identity reset, rows predating it match neither endpoint, so past sends read as incoming from the device's own retired identity. Fixing that needs `role` on the core's TargetedTransfer.
Localization
Single source of truth for every user-facing string, in strings.json.
A Bun CLI generates the platform-native files from it:
| Target | Output | Notes |
|---|---|---|
apple |
apple/VniDrop/Resources/Localizable.xcstrings |
one catalog, all languages nested |
kmp |
shared/src/commonMain/composeResources/values[-lang]/strings.xml |
one file per language |
Workflow
# From the repository root:
make check-localization # structural checks (run before committing)
make localization # regenerate .xcstrings + strings.xml from strings.json
make localization-migrate # one-time: rebuild strings.json from platform files
Never edit the generated .xcstrings / strings.xml by hand — edit strings.json and
regenerate. Regenerated output is deterministic (sorted keys), so diffs stay small.
strings.json format
{
"sourceLanguage": "en",
"supportedLanguages": ["en", "fr"],
"strings": {
"send_title": {
"context": "Send tab — screen title.",
"translations": { "en": "Send", "fr": "Envoyer" }
},
"send_selected_files_count": {
"context": "Send flow — number of files chosen before creating a transfer.",
"targets": ["kmp", "apple"],
"args": [{ "name": "count", "type": "int" }],
"plural": {
"en": { "one": "{count} file selected", "other": "{count} files selected" },
"fr": { "one": "{count} fichier sélectionné", "other": "{count} fichiers sélectionnés" }
}
}
}
}
Fields
context(required) — where the string appears and its purpose. Emitted as the.xcstringscomment and an XML comment; also the note translators see.targets(optional) —["kmp", "apple"]. Omit to mean all targets.args(optional) — ordered list of{ name, type },type∈string | int | double. Referenced in text as{name}.translations— flat text per language. Mutually exclusive withplural.plural— per language, per CLDR category (zero,one,two,few,many,other).otheris always required.
Placeholders
Write named tokens {count}, {name} in text. The generator converts them to the right
positional token per platform, using the declared type:
| type | Apple | Android/KMP |
|---|---|---|
string |
%N$@ |
%N$s |
int |
%N$d |
%N$d |
double |
%N$f |
%N$f |
A literal % in text is emitted as %% whenever the string has args.
Adding a language
Add its code to supportedLanguages, fill in translations / plural for each key, then
generate. KMP gets a new values-<lang>/strings.xml; Apple gets the language inside the
single catalog. validate warns about any key still missing that language.
Migration notes (from the initial import)
- Apple keys that were literal English strings (
"%@ · %@") were imported verbatim — rename them to semantic keys and update the Swift call sites. - Arg names default to
arg1,arg2… (a lone int arg becomescount). Rename for clarity; keep the{token}in text in sync. - Folding
transfer_file_count_one/_otherinto the plural keytransfer_file_countrequires switching the KMP call site fromRes.string.transfer_file_count_oneto the Compose plural API (pluralStringResource(Res.plurals.transfer_file_count, count, count)), and the Apple side to automatic plural inflection.