refactor: every count in the web app is said by one plural(), with the viewer's thousands separator; a source guard names any hand-written copy (M505 #5642)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 16:18:04 -04:00
co-authored by Claude Opus 5.5
parent af2d4f9859
commit 9e24b2b4fc
34 changed files with 144 additions and 75 deletions
@@ -1,4 +1,5 @@
<script lang="ts">
import { plural } from '#lib/utils/plural.js';
import { Download as DownloadIcon } from 'lucide-svelte';
import { useQueryClient } from '@tanstack/svelte-query';
import {
@@ -138,7 +139,7 @@
function filed(d: Download): string | null {
if (d.import_state !== 'imported' && d.import_state !== 'held') return null;
let said = d.imported_files === 1 ? '1 track filed' : `${d.imported_files} tracks filed`;
let said = plural(d.imported_files, 'track filed', 'tracks filed');
if (d.skipped) said += `, ${d.skipped} already held`;
if (d.destination) said += ` into ${d.destination}`;
return said;
@@ -1,4 +1,5 @@
<script lang="ts">
import { plural } from '#lib/utils/plural.js';
import { Disc3 } from 'lucide-svelte';
import { useQueryClient } from '@tanstack/svelte-query';
import {
@@ -85,7 +86,7 @@
{/if}
</div>
<div class="text-xs text-text-secondary">
{f.open_wants === 1 ? '1 album wanted' : `${f.open_wants} albums wanted`} ·
{plural(f.open_wants, 'album wanted', 'albums wanted')} ·
{f.checked_at ? `checked ${relativeTime(f.checked_at)}` : 'not checked yet'}
</div>
</div>
@@ -1,4 +1,5 @@
<script lang="ts">
import { plural } from '#lib/utils/plural.js';
import { CircleCheck } from 'lucide-svelte';
import { useQueryClient } from '@tanstack/svelte-query';
import { createUnmatchedQuery, leaveUnmatchedOff, matchUnmatched, putUnmatchedBack } from '#lib/api/wanted.js';
@@ -57,7 +58,6 @@
return `${u.title} is back to be matched.`;
});
const tracks = (n: number) => (n === 1 ? '1 track' : `${n} tracks`);
</script>
{#if unmatched.isPending}
@@ -89,7 +89,7 @@
<a href="/albums/{u.album_id}" class="block truncate text-sm font-medium text-text-primary hover:underline"
>{u.title}</a
>
<div class="truncate text-xs text-text-secondary">{u.artist} · {tracks(u.tracks)}</div>
<div class="truncate text-xs text-text-secondary">{u.artist} · {plural(u.tracks, 'track', 'tracks')}</div>
<p class="text-xs text-text-muted">{u.detail}</p>
</div>
<div class="flex shrink-0 gap-2">
@@ -131,7 +131,7 @@
<div class="min-w-0 flex-1 space-y-0.5">
<a href="/albums/{u.album_id}" class="block truncate text-sm text-text-primary hover:underline">{u.title}</a>
<div class="truncate text-xs text-text-secondary">
{u.artist} · {tracks(u.tracks)} · left off {relativeTime(u.decided_at)}
{u.artist} · {plural(u.tracks, 'track', 'tracks')} · left off {relativeTime(u.decided_at)}
</div>
</div>
<button
@@ -1,4 +1,5 @@
<script lang="ts">
import { plural } from '#lib/utils/plural.js';
import { Album, ChevronDown, ListChecks, ListMusic, Search, X } from 'lucide-svelte';
import { useQueryClient } from '@tanstack/svelte-query';
import { createWantsQuery, removeWant, searchWant } from '#lib/api/wanted.js';
@@ -98,8 +99,7 @@
function arrived(w: Want): string | null {
const g = w.grab;
if (!g || g.import_state !== 'imported' || !g.imported_at) return null;
const n = g.imported_files === 1 ? '1 track' : `${g.imported_files} tracks`;
return `Imported ${n} ${relativeTime(g.imported_at)} from ${g.name}`;
return `Imported ${plural(g.imported_files, 'track', 'tracks')} ${relativeTime(g.imported_at)} from ${g.name}`;
}
// A want the schedule still searches for: open, or held but wanted for a
@@ -119,7 +119,6 @@
return 'Its files went missing';
}
const albums = (n: number) => (n === 1 ? '1 album' : `${n} albums`);
function kind(w: Want): string {
const parts = [w.primary_type, ...w.secondary_types].filter(Boolean);
@@ -141,7 +140,7 @@
<a href="/collection" class="text-xs text-text-secondary hover:text-text-primary">← Catalogue</a>
<h2 class="truncate text-xl font-medium text-text-primary">{head.artist_name}</h2>
<p class="text-sm text-text-secondary" data-testid="catalogue-artist-counts">
{head.held} of {albums(head.albums)} held{head.wanted ? ` · ${head.wanted} wanted` : ''}{head.missing
{head.held} of {plural(head.albums, 'album', 'albums')} held{head.wanted ? ` · ${head.wanted} wanted` : ''}{head.missing
? ` · ${head.missing} with files missing`
: ''}{head.downloading ? ` · ${head.downloading} downloading` : ''}
</p>
@@ -200,7 +199,7 @@
{a.name}
{/if}
</span>
<span class="shrink-0 text-xs text-text-muted">{albums(a.wants.length)}</span>
<span class="shrink-0 text-xs text-text-muted">{plural(a.wants.length, 'album', 'albums')}</span>
</h2>{/if}
<ul class="divide-y divide-border rounded-lg border border-border bg-surface">
{#each a.wants as w (w.id)}