From e4fa3b660ee7adcb4cba75de0421475e6e259ea3 Mon Sep 17 00:00:00 2001 From: SandObserver <260779319+SandObserver@users.noreply.github.com> Date: Tue, 27 Oct 2026 06:36:49 -0400 Subject: [PATCH] Name the Downloads finish day in the dashboard language Move the finish-time formatting into belt-logic.js. The weekday and month follow the dashboard language; the 12- or 24-hour clock still follows the reader's locale. Remove the SABnzbd text check that a 200 reply could never reach. --- ui/test/downloads-belt.test.mjs | 18 ++++++++++++++++ ui/widgets/downloads/belt-logic.js | 33 ++++++++++++++++++++++++++++++ ui/widgets/downloads/data.js | 2 -- ui/widgets/downloads/index.html | 23 +++++---------------- 4 files changed, 56 insertions(+), 20 deletions(-) diff --git a/ui/test/downloads-belt.test.mjs b/ui/test/downloads-belt.test.mjs index 49c6544e..ba981c53 100644 --- a/ui/test/downloads-belt.test.mjs +++ b/ui/test/downloads-belt.test.mjs @@ -2,11 +2,13 @@ import assert from 'node:assert/strict'; import { test } from 'node:test'; import { ENDS, + finishParts, finishedToday, hoursMinutes, leftover, LEFTOVER_CHANCE, lidOpen, + readerHourCycle, startOfDay, variations, } from '../widgets/downloads/belt-logic.js'; @@ -63,3 +65,19 @@ test('leftovers appear about one load in ten and stay inside the belt', () => { assert.deepEqual([...kinds].sort(), ['cutter', 'roll']); assert.deepEqual(leftover(42), leftover(42)); }); + +test('a finish time names the day in the interface language and keeps the reader clock', () => { + const now = new Date(2026, 9, 27, 9, 0).getTime(); + const fri = new Date(2026, 9, 30, 13, 5).getTime(); + const us = readerHourCycle('en-US'); + assert.deepEqual(finishParts(fri, now, 'de', us), { time: 'Fr. 1:05', half: 'pm' }); + assert.deepEqual(finishParts(fri, now, 'en', readerHourCycle('de-DE')), { time: 'Fri 13:05', half: '' }); + assert.match(finishParts(fri, now, 'fa', us).time, /^جمعه /); +}); + +test('a finish time today shows only the clock, and a week out only the date', () => { + const now = new Date(2026, 9, 27, 9, 0).getTime(); + const us = readerHourCycle('en-US'); + assert.deepEqual(finishParts(new Date(2026, 9, 27, 9, 30).getTime(), now, 'en', us), { time: '9:30', half: 'am' }); + assert.deepEqual(finishParts(new Date(2026, 10, 9, 9, 30).getTime(), now, 'de', us), { time: '9. Nov.', half: '' }); +}); diff --git a/ui/widgets/downloads/belt-logic.js b/ui/widgets/downloads/belt-logic.js index c4b90530..6ed3afdd 100644 --- a/ui/widgets/downloads/belt-logic.js +++ b/ui/widgets/downloads/belt-logic.js @@ -40,6 +40,39 @@ export function startOfDay(now) { return d.getTime(); } +/** The 12- or 24-hour clock the reader's own locale uses. + @param {string} [locale] @returns {string|undefined} */ +export function readerHourCycle(locale) { + try { + return new Intl.DateTimeFormat(locale || 'en', { hour: 'numeric' }).resolvedOptions().hourCycle; + } catch { + return undefined; + } +} + +/** A finish time in the interface language, without its day period. `half` is + 'am' or 'pm' when the clock has one, so the caller names it from its catalog. + @param {number} ms @param {number} now @param {string} lang @param {string|undefined} hourCycle + @returns {{ time: string, half: ''|'am'|'pm' }} */ +export function finishParts(ms, now, lang, hourCycle) { + const days = (startOfDay(ms) - startOfDay(now)) / 86400000; + /** @type {Intl.DateTimeFormatOptions} */ + const opts = + days < 1 + ? { hour: 'numeric', minute: '2-digit', hourCycle } + : days < 7 + ? { weekday: 'short', hour: 'numeric', minute: '2-digit', hourCycle } + : { month: 'short', day: 'numeric' }; + const parts = new Intl.DateTimeFormat(lang, opts).formatToParts(ms); + const time = parts + .filter(p => p.type !== 'dayPeriod') + .map(p => p.value) + .join('') + .trim(); + if (!parts.some(p => p.type === 'dayPeriod')) return { time, half: '' }; + return { time, half: new Date(ms).getHours() < 12 ? 'am' : 'pm' }; +} + /** How many finished today, and the newest. @param {number[]} finished epoch ms @param {number} now @returns {{ count: number, last: number|null }} */ diff --git a/ui/widgets/downloads/data.js b/ui/widgets/downloads/data.js index 0426d8a2..34a53e3c 100644 --- a/ui/widgets/downloads/data.js +++ b/ui/widgets/downloads/data.js @@ -113,8 +113,6 @@ async function sabnzbd(ctx) { }); if (r.status === 401 || r.status === 403) ctx.fail('SABnzbd refused the API key', { kind: ctx.KIND.AUTH }); if (r.status !== 200) ctx.fail(`SABnzbd returned HTTP ${r.status}`, { kind: ctx.KIND.UPSTREAM }); - if (typeof r.data === 'string' && /api key/i.test(r.data)) - ctx.fail('SABnzbd refused the API key', { kind: ctx.KIND.AUTH }); if (!r.data || typeof r.data !== 'object' || !r.data[mode]) ctx.fail('SABnzbd sent a reply in an unexpected shape', { kind: ctx.KIND.UPSTREAM }); return r.data[mode]; diff --git a/ui/widgets/downloads/index.html b/ui/widgets/downloads/index.html index 4f630698..7c1ec266 100644 --- a/ui/widgets/downloads/index.html +++ b/ui/widgets/downloads/index.html @@ -105,7 +105,7 @@ poll, getConfig, loadStrings, wt, wtCount, formatNumber, formatList, localiseDigits, linkTo, errorState, readingTime, } from '/js/widget-toolbox.js?v=66813091'; import { ensureSprite } from '/js/icon-set.js?v=26655aca'; -import { variations, finishedToday, hoursMinutes, leftover, lidOpen, startOfDay } from './belt-logic.js'; +import { variations, finishedToday, hoursMinutes, leftover, lidOpen, finishParts, readerHourCycle } from './belt-logic.js'; const SVG = 'http://www.w3.org/2000/svg'; const TONES = [ @@ -190,25 +190,12 @@ return bytes >= 1e9 ? unit(bytes / 1e9, 'gigabyte', 1) : unit(bytes / 1e6, 'megabyte'); } const percent = p => iso(formatNumber(p, { style: 'percent', maximumFractionDigits: 0 })); +const LANG = new URLSearchParams(location.search).get('lang') || 'en'; +const HOUR_CYCLE = readerHourCycle(navigator.language); function clockParts(ms) { try { - const days = (startOfDay(ms) - startOfDay(Date.now())) / 86400000; - const opts = - days < 1 - ? { hour: 'numeric', minute: '2-digit' } - : days < 7 - ? { weekday: 'short', hour: 'numeric', minute: '2-digit' } - : { month: 'short', day: 'numeric' }; - const parts = new Intl.DateTimeFormat(navigator.language || 'en', opts).formatToParts(ms); - const time = parts.filter(p => p.type !== 'dayPeriod').map(p => p.value).join('').trim(); - const found = parts.find(p => p.type === 'dayPeriod')?.value || ''; - /* The reader's locale picks a 12-hour clock; the interface language names its halves. */ - const morning = new Date(ms).getHours() < 12; - const period = !found - ? '' - : morning - ? wt('ui.am', 'AM') - : wt('ui.pm', 'PM'); + const { time, half } = finishParts(ms, Date.now(), LANG, HOUR_CYCLE); + const period = half === 'am' ? wt('ui.am', 'AM') : half === 'pm' ? wt('ui.pm', 'PM') : ''; return { time: localiseDigits(time), period }; } catch { return { time: localiseDigits(new Date(ms).toTimeString().slice(0, 5)), period: '' };