Real plurals: the web's sync messages and Android's tag counts

Web: one counted(n, one, other) helper. The sync screen's three "(s)"
messages (attachments that didn't download, changes refused, files that
didn't upload) now say "1 file" / "2 files". The six hand-written ternaries
(SyncView x2, ImportNotes, GroupList, ShareDialog, LabelsModal) use it too.

Android: tags_count was two strings chosen in code, and is now <plurals>.
The delete confirmation read "It is on 1 notes … removed from all of them"
and now has a one form of its own.

Operator decision on #5371, 2026-10-08.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 16:57:09 -04:00
co-authored by Claude Opus 5.5
parent 29dd6dd351
commit 921c0a47bd
9 changed files with 45 additions and 17 deletions
+7 -5
View File
@@ -25,6 +25,7 @@ import {
import { errorMessage } from "../api/errors";
import { formatDateTime } from "../notes/datetime";
import { toastOnFailure } from "../composables/useAction";
import { counted } from "../counted";
// Opt-in server sync for the desktop app. Being UNLINKED is the normal resting
// state, not an incomplete setup — the app is local-first and fully usable having
@@ -313,14 +314,15 @@ function showOutcome(outcome: SyncOutcome) {
const parts: string[] = [];
if (sent > 0) parts.push(`sent ${sent}`);
const up = outcome.push.uploaded;
if (up > 0) parts.push(`uploaded ${up} file${up === 1 ? "" : "s"}`);
if (up > 0) parts.push(`uploaded ${counted(up, "file", "files")}`);
if (received > 0) parts.push(`received ${received}`);
if (blobs > 0) parts.push(`${blobs} attachment${blobs === 1 ? "" : "s"}`);
if (blobs > 0) parts.push(counted(blobs, "attachment", "attachments"));
lastResult.value = parts.length ? `Synced — ${parts.join(", ")}.` : "Already up to date.";
// Attachments that didn't arrive are retried next sync, so this is a note, not
// an error — but saying nothing would leave a missing image unexplained.
if (outcome.pull.blobs_failed > 0) {
lastResult.value += ` ${outcome.pull.blobs_failed} attachment(s) didn't download — they'll retry on the next sync.`;
const failed = counted(outcome.pull.blobs_failed, "attachment", "attachments");
lastResult.value += ` ${failed} didn't download — they'll retry on the next sync.`;
}
// Rejections are the server refusing a specific change — surfaced, never
// swallowed, because only the person can resolve them. The background cycle does
@@ -328,8 +330,8 @@ function showOutcome(outcome: SyncOutcome) {
// A file refused for good is listed once, here, and not retried; the editor says so
// on the file itself from then on.
const problems: string[] = [];
if (outcome.push.rejected > 0) problems.push(`${outcome.push.rejected} change(s) the server wouldn't accept`);
if (outcome.push.upload_failed > 0) problems.push(`${outcome.push.upload_failed} file(s) didn't upload`);
if (outcome.push.rejected > 0) problems.push(`${counted(outcome.push.rejected, "change", "changes")} the server wouldn't accept`);
if (outcome.push.upload_failed > 0) problems.push(`${counted(outcome.push.upload_failed, "file", "files")} didn't upload`);
syncError.value = problems.length ? `${problems.join(" and ")}: ${outcome.push.errors.join("; ")}` : "";
}