2d5c12ca30
French is a complete table, not a partial one: 435 keys, the same set German
and Portuguese carry (English's 443 minus the ten Polish-only .few/.many forms
and the bare upload.skippedFiles, plus singular forms for the three
playlist.skip.* families). French takes the one/other buckets, so plural()
needs no change.
Verified with the checks from .claude/rules/i18n.md: the drift check reports
clean, and separately there are zero {placeholder} mismatches and zero HTML tag
mismatches against English. The 27 strings identical to English are genuinely
identical in French (Piano, Solo, Transport, Position, LUFS, Standard, Port,
the brand names, CUDA (NVIDIA), MPS (Apple Silicon)).
Separately: the runtime id was being computed from the raw bytes of uv.lock, so
a Windows checkout with core.autocrlf=true hashed CRLF and Linux hashed LF, and
the same lockfile produced two different ids -- caught by building the Linux
package and seeing py3.12-d74d6ef80c5e9d1f where Windows had produced
py3.12-dbda45e38e1044cf. Each platform stayed self-consistent so the gate still
worked, but the id would shift spuriously if a runner's autocrlf ever changed,
silently declining app-only updates that were in fact compatible. Both scripts
now hash the content with newlines normalised; PowerShell, bash and a reference
Python implementation all agree on d74d6ef80c5e9d1f.