From 428d900af6222a331bd1b823aa29bd5b3f879c67 Mon Sep 17 00:00:00 2001 From: SandObserver <260779319+SandObserver@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:07:56 -0400 Subject: [PATCH] Join lists without a conjunction in the interface language --- ui/admin/index.html | 2 +- ui/index.html | 2 +- ui/js/admin-app-form.js | 12 +++++------ ui/js/admin-auth.js | 8 +++---- ui/js/admin-color-control.js | 6 +++--- ui/js/admin-drag.js | 2 +- ui/js/admin-list.js | 10 ++++----- ui/js/admin-settings.js | 12 +++++------ ui/js/admin-shared.js | 8 +++---- ui/js/admin-widget-form.js | 10 ++++----- ui/js/admin.js | 24 ++++++++++----------- ui/js/background.js | 2 +- ui/js/badge-popover.js | 2 +- ui/js/config-recovery.js | 2 +- ui/js/dashboard.js | 16 +++++++------- ui/js/format-number.js | 7 ++++-- ui/js/i18n.js | 2 +- ui/js/listbox.js | 6 +++--- ui/js/spotlight.js | 4 ++-- ui/js/ui.js | 4 ++-- ui/js/utils.js | 2 +- ui/js/widget-config-form.js | 12 +++++------ ui/js/widget-toolbox.js | 4 ++-- ui/test/localised-digits.test.mjs | 20 ++++++++++++++++- ui/widgets/backup/backup.html | 4 ++-- ui/widgets/books/index.html | 2 +- ui/widgets/clock/analog.html | 2 +- ui/widgets/clock/digital.html | 2 +- ui/widgets/connections/connections-map.html | 10 ++++----- ui/widgets/connections/connections-vpn.html | 2 +- ui/widgets/dashboard-switch/index.html | 2 +- ui/widgets/disk-health/index.html | 2 +- ui/widgets/dns/index.html | 2 +- ui/widgets/github/contributions.html | 2 +- ui/widgets/github/pullrequests.html | 4 ++-- ui/widgets/nowplaying/index.html | 2 +- ui/widgets/system-summary/index.html | 12 ++--------- ui/widgets/weather/index.html | 2 +- 38 files changed, 121 insertions(+), 108 deletions(-) diff --git a/ui/admin/index.html b/ui/admin/index.html index 67fc16f0..6228512d 100644 --- a/ui/admin/index.html +++ b/ui/admin/index.html @@ -301,6 +301,6 @@

General

- + diff --git a/ui/index.html b/ui/index.html index 37c32d39..e756e45d 100644 --- a/ui/index.html +++ b/ui/index.html @@ -56,5 +56,5 @@ - + diff --git a/ui/js/admin-app-form.js b/ui/js/admin-app-form.js index 9ed4b5fc..9a8ee203 100644 --- a/ui/js/admin-app-form.js +++ b/ui/js/admin-app-form.js @@ -1,9 +1,9 @@ -import { clr, el, focusFirst, initial, inp as inpById, isolate, q as qSel, qa, qi, tgt } from '/js/utils.js?v=b6e6c040'; +import { clr, el, focusFirst, initial, inp as inpById, isolate, q as qSel, qa, qi, tgt } from '/js/utils.js?v=14b18b8c'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; import { loadLocalIcons, resolveIcon, iconChain, cdnIconRef, splitIconRef } from '/js/icons.js?v=9c7b5111'; import { state } from '/js/admin-state.js?v=af772a1b'; import { DOCK_MAX } from '/js/limits.js?v=31048a24'; -import { formatNumber } from '/js/format-number.js?v=52226e9c'; +import { formatNumber } from '/js/format-number.js?v=abcd4ae4'; import { isDockBlocked, clearsStoredSecret, @@ -12,7 +12,7 @@ import { nextActiveIndex, sameIconName, } from '/js/admin-logic.js?v=fc7f0836'; -import { t } from '/js/i18n.js?v=f754f6fe'; +import { t } from '/js/i18n.js?v=ed7c5565'; import { toast, errorText, @@ -24,10 +24,10 @@ import { reveal, setTogDisabled, swapContent, -} from '/js/admin-shared.js?v=38ed2b51'; -import { createListbox } from '/js/listbox.js?v=ae7b9a38'; +} from '/js/admin-shared.js?v=36dc5cc1'; +import { createListbox } from '/js/listbox.js?v=23e87940'; import { MAX_LABELS } from '/js/badge-logic.js?v=5c72bd4c'; -import { renderColorControl, BADGE_DEFAULT } from '/js/admin-color-control.js?v=44fdebde'; +import { renderColorControl, BADGE_DEFAULT } from '/js/admin-color-control.js?v=e80f4a68'; import { badgeErrorAdvice, TONE } from '/js/admin-error.js?v=b0881957'; import { fluidHoverClear, fluidHoverKb } from '/js/fluid-hover.js?v=cb886e86'; import { iconSvg } from '/js/icon-set.js?v=628341d4'; diff --git a/ui/js/admin-auth.js b/ui/js/admin-auth.js index 2ebc608f..1513ab2c 100644 --- a/ui/js/admin-auth.js +++ b/ui/js/admin-auth.js @@ -1,9 +1,9 @@ -import { apiGet, apiPost } from '/js/admin-shared.js?v=38ed2b51'; -import { initI18n, t } from '/js/i18n.js?v=f754f6fe'; +import { apiGet, apiPost } from '/js/admin-shared.js?v=36dc5cc1'; +import { initI18n, t } from '/js/i18n.js?v=ed7c5565'; import { loginErrorKey } from '/js/admin-error.js?v=b0881957'; import { pwStrength } from '/js/password-strength.js?v=389e0ed0'; -import { el, inp as inpById, qa } from '/js/utils.js?v=b6e6c040'; -import { blockingScreenFor } from '/js/config-recovery.js?v=e22b5e5a'; +import { el, inp as inpById, qa } from '/js/utils.js?v=14b18b8c'; +import { blockingScreenFor } from '/js/config-recovery.js?v=85138697'; export async function checkAuth(onLogin) { try { diff --git a/ui/js/admin-color-control.js b/ui/js/admin-color-control.js index 375c5882..87b07e0d 100644 --- a/ui/js/admin-color-control.js +++ b/ui/js/admin-color-control.js @@ -1,7 +1,7 @@ -import { PE_SVG, initInlineEdit, toast, reveal } from '/js/admin-shared.js?v=38ed2b51'; -import { t } from '/js/i18n.js?v=f754f6fe'; +import { PE_SVG, initInlineEdit, toast, reveal } from '/js/admin-shared.js?v=36dc5cc1'; +import { t } from '/js/i18n.js?v=ed7c5565'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; -import { qa, q } from '/js/utils.js?v=b6e6c040'; +import { qa, q } from '/js/utils.js?v=14b18b8c'; import { iconSvg } from '/js/icon-set.js?v=628341d4'; import { HUE_NAMES, ROLE_NAMES, TILE_KEYWORDS, pageTheme, tileColor } from '/js/palette.js?v=3fb8ae43'; diff --git a/ui/js/admin-drag.js b/ui/js/admin-drag.js index efb695a7..3b98413c 100644 --- a/ui/js/admin-drag.js +++ b/ui/js/admin-drag.js @@ -12,7 +12,7 @@ import { applyDrop, canJoinFolder, folderRowZone } from '/js/admin-drag-logic.js?v=6b767e76'; import { state } from '/js/admin-state.js?v=af772a1b'; import { snapshotItems } from '/js/admin-save-logic.js?v=5811620e'; -import { qa } from '/js/utils.js?v=b6e6c040'; +import { qa } from '/js/utils.js?v=14b18b8c'; /** @type {(before: unknown) => void} */ let _save = () => {}; diff --git a/ui/js/admin-list.js b/ui/js/admin-list.js index ea660f0d..1e854410 100644 --- a/ui/js/admin-list.js +++ b/ui/js/admin-list.js @@ -14,12 +14,12 @@ import { collapsedFolders, filter, state } from '/js/admin-state.js?v=af772a1b'; import { snapshotItems } from '/js/admin-save-logic.js?v=5811620e'; import { reorderItems } from '/js/admin-logic.js?v=fc7f0836'; -import { initDrag, wireRowDrag } from '/js/admin-drag.js?v=d522e526'; -import { paintIcon } from '/js/admin-shared.js?v=38ed2b51'; -import { clr, el, focusFirst, initial, qa, setUserText } from '/js/utils.js?v=b6e6c040'; +import { initDrag, wireRowDrag } from '/js/admin-drag.js?v=b2422767'; +import { paintIcon } from '/js/admin-shared.js?v=36dc5cc1'; +import { clr, el, focusFirst, initial, qa, setUserText } from '/js/utils.js?v=14b18b8c'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; -import { t } from '/js/i18n.js?v=f754f6fe'; -import { sizeLabel } from '/js/admin-widget-form.js?v=0879c8a7'; +import { t } from '/js/i18n.js?v=ed7c5565'; +import { sizeLabel } from '/js/admin-widget-form.js?v=8a6be17f'; import { widgetGlyph } from '/js/widget-glyphs.js?v=b383b7c8'; import { iconSvg } from '/js/icon-set.js?v=628341d4'; diff --git a/ui/js/admin-settings.js b/ui/js/admin-settings.js index aa48a8d8..8e7f53b1 100644 --- a/ui/js/admin-settings.js +++ b/ui/js/admin-settings.js @@ -1,7 +1,7 @@ -import { toast, apiGet, apiPost, errorText, reveal, swapContent } from '/js/admin-shared.js?v=38ed2b51'; +import { toast, apiGet, apiPost, errorText, reveal, swapContent } from '/js/admin-shared.js?v=36dc5cc1'; import { socketProbeAdvice } from '/js/admin-error.js?v=b0881957'; import { pwStrength } from '/js/password-strength.js?v=389e0ed0'; -import { t } from '/js/i18n.js?v=f754f6fe'; +import { t } from '/js/i18n.js?v=ed7c5565'; import { shouldWritePassword, settingsSaveBlocker, @@ -11,11 +11,11 @@ import { BLOCK, } from '/js/admin-logic.js?v=fc7f0836'; import { confirmText, promptModal } from '/js/modal.js?v=6b0320bd'; -import { el, inp, setUserText } from '/js/utils.js?v=b6e6c040'; -import { formatNumber } from '/js/format-number.js?v=52226e9c'; +import { el, inp, setUserText } from '/js/utils.js?v=14b18b8c'; +import { formatNumber } from '/js/format-number.js?v=abcd4ae4'; import { serialWrites } from '/js/admin-save-logic.js?v=5811620e'; -import { renderColorControl } from '/js/admin-color-control.js?v=44fdebde'; -import { BACKDROP } from '/js/background.js?v=1bd5f25e'; +import { renderColorControl } from '/js/admin-color-control.js?v=e80f4a68'; +import { BACKDROP } from '/js/background.js?v=0f4ca726'; import { ALLOWED_HOSTS_MAX, firstBadHost, diff --git a/ui/js/admin-shared.js b/ui/js/admin-shared.js index 799bb387..0f0706fd 100644 --- a/ui/js/admin-shared.js +++ b/ui/js/admin-shared.js @@ -1,12 +1,12 @@ /* Stateless helpers shared by the admin modules. Mutable state stays out. */ import { recoversSession, toastHoldMs } from '/js/admin-logic.js?v=fc7f0836'; -import { el, isolate, q } from '/js/utils.js?v=b6e6c040'; -import { t } from '/js/i18n.js?v=f754f6fe'; +import { el, isolate, q } from '/js/utils.js?v=14b18b8c'; +import { t } from '/js/i18n.js?v=ed7c5565'; import { errorAdvice } from '/js/admin-error.js?v=b0881957'; -import { formatList, formatNumber } from '/js/format-number.js?v=52226e9c'; +import { formatList, formatNumber } from '/js/format-number.js?v=abcd4ae4'; import { iconChain } from '/js/icons.js?v=9c7b5111'; import { iconSvg } from '/js/icon-set.js?v=628341d4'; -import { blockingScreenFor, recoveryShown, showBlockingScreen } from '/js/config-recovery.js?v=e22b5e5a'; +import { blockingScreenFor, recoveryShown, showBlockingScreen } from '/js/config-recovery.js?v=85138697'; export const API = ''; diff --git a/ui/js/admin-widget-form.js b/ui/js/admin-widget-form.js index 7a53c324..19a54313 100644 --- a/ui/js/admin-widget-form.js +++ b/ui/js/admin-widget-form.js @@ -1,11 +1,11 @@ import { state } from '/js/admin-state.js?v=af772a1b'; -import { PE_SVG, initInlineEdit, swapContent } from '/js/admin-shared.js?v=38ed2b51'; -import { createListbox } from '/js/listbox.js?v=ae7b9a38'; -import { renderWidgetConfigForm } from '/js/widget-config-form.js?v=8c3f820d'; +import { PE_SVG, initInlineEdit, swapContent } from '/js/admin-shared.js?v=36dc5cc1'; +import { createListbox } from '/js/listbox.js?v=23e87940'; +import { renderWidgetConfigForm } from '/js/widget-config-form.js?v=191cb80f'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; import { sizesForView, widgetConfigMode, rejectionLines, carriesTypedValues } from '/js/admin-logic.js?v=fc7f0836'; -import { t } from '/js/i18n.js?v=f754f6fe'; -import { focusFirst, q, qi, qa } from '/js/utils.js?v=b6e6c040'; +import { t } from '/js/i18n.js?v=ed7c5565'; +import { focusFirst, q, qi, qa } from '/js/utils.js?v=14b18b8c'; import { iconSvg } from '/js/icon-set.js?v=628341d4'; const SIZES_WITH_ICONS = new Set(['small', 'medium', 'large', 'xlarge']); diff --git a/ui/js/admin.js b/ui/js/admin.js index d3f351a0..3eaf7152 100644 --- a/ui/js/admin.js +++ b/ui/js/admin.js @@ -1,7 +1,7 @@ -import { buildAppForm, buildFolderForm, captureActLabels, serializeKvRows } from '/js/admin-app-form.js?v=a896a105'; -import { checkAuth, requireLogin, wirePasswordStrength } from '/js/admin-auth.js?v=8aee98e6'; -import { recoveryShown } from '/js/config-recovery.js?v=e22b5e5a'; -import { focusRow, initList, render, syncFilterUI } from '/js/admin-list.js?v=547999d2'; +import { buildAppForm, buildFolderForm, captureActLabels, serializeKvRows } from '/js/admin-app-form.js?v=dd5f148f'; +import { checkAuth, requireLogin, wirePasswordStrength } from '/js/admin-auth.js?v=71f8a57a'; +import { recoveryShown } from '/js/config-recovery.js?v=85138697'; +import { focusRow, initList, render, syncFilterUI } from '/js/admin-list.js?v=5bdad01b'; import { resolveAdminSection } from '/js/admin-logic.js?v=fc7f0836'; import { afterImport, @@ -21,7 +21,7 @@ import { settingsDirty, showBgFields, showWallpaperFile, -} from '/js/admin-settings.js?v=a310b96a'; +} from '/js/admin-settings.js?v=d69636fc'; import { apiGet, apiPost, @@ -35,15 +35,15 @@ import { setReauthHandler, toast, withheldText, -} from '/js/admin-shared.js?v=38ed2b51'; +} from '/js/admin-shared.js?v=36dc5cc1'; import { collapsedFolders, filter, state } from '/js/admin-state.js?v=af772a1b'; -import { buildWidgetForm } from '/js/admin-widget-form.js?v=0879c8a7'; +import { buildWidgetForm } from '/js/admin-widget-form.js?v=8a6be17f'; import { initFluidHover } from '/js/fluid-hover.js?v=cb886e86'; -import { formatNumber } from '/js/format-number.js?v=52226e9c'; +import { formatNumber } from '/js/format-number.js?v=abcd4ae4'; import { initGlideSelect, syncGlideSelect } from '/js/glide-select.js?v=8b39e9d0'; -import { createListbox } from '/js/listbox.js?v=ae7b9a38'; +import { createListbox } from '/js/listbox.js?v=23e87940'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; -import { initI18n, LANGUAGES, t } from '/js/i18n.js?v=f754f6fe'; +import { initI18n, LANGUAGES, t } from '/js/i18n.js?v=ed7c5565'; import { loadLocalIcons } from '/js/icons.js?v=9c7b5111'; import { ensureSprite, iconSvg } from '/js/icon-set.js?v=628341d4'; import { @@ -80,8 +80,8 @@ import { storeGet, storeSet, tgt, -} from '/js/utils.js?v=b6e6c040'; -import { applyBackground, resolveBackground } from '/js/background.js?v=1bd5f25e'; +} from '/js/utils.js?v=14b18b8c'; +import { applyBackground, resolveBackground } from '/js/background.js?v=0f4ca726'; import { parseYamlTolerant, YamlLiteError, YamlTooLargeError } from '/js/yaml-lite.js?v=a42ef914'; ensureSprite(); diff --git a/ui/js/background.js b/ui/js/background.js index 7e6928c4..10ca4f1a 100644 --- a/ui/js/background.js +++ b/ui/js/background.js @@ -1,5 +1,5 @@ // @ts-check -import { cssColor, sanitizeCssUrl } from '/js/utils.js?v=b6e6c040'; +import { cssColor, sanitizeCssUrl } from '/js/utils.js?v=14b18b8c'; import { loadWallpaper, saveWallpaper } from '/js/wallpaper-cache.js?v=c5f8a3e6'; export const BACKDROP = '#0d1117'; diff --git a/ui/js/badge-popover.js b/ui/js/badge-popover.js index 8a41a011..88679977 100644 --- a/ui/js/badge-popover.js +++ b/ui/js/badge-popover.js @@ -2,7 +2,7 @@ /* One element for the whole dashboard. A per-tile popover outlives the tiles the grid rebuilds on every resize. */ -import { formatNumber } from '/js/format-number.js?v=52226e9c'; +import { formatNumber } from '/js/format-number.js?v=abcd4ae4'; const HOVER_IN_MS = 320; const COMPAT_CLICK_MS = 500; diff --git a/ui/js/config-recovery.js b/ui/js/config-recovery.js index 1cc97168..61b22541 100644 --- a/ui/js/config-recovery.js +++ b/ui/js/config-recovery.js @@ -1,6 +1,6 @@ // @ts-check import { esc, html, raw, setHtml } from '/js/html.js?v=c71f8903'; -import { LANGUAGES, initI18n, t } from '/js/i18n.js?v=f754f6fe'; +import { LANGUAGES, initI18n, t } from '/js/i18n.js?v=ed7c5565'; import { sanitizeI18nMarkup } from '/js/i18n-markup.js?v=8c90e1dd'; import { fillNames, pickLanguage, screenFor } from '/js/config-recovery-logic.js?v=e6e24763'; diff --git a/ui/js/dashboard.js b/ui/js/dashboard.js index 36cf1f30..22ef102c 100644 --- a/ui/js/dashboard.js +++ b/ui/js/dashboard.js @@ -28,12 +28,12 @@ import { storeSet, teardownWidgets, titleWhenTruncated, -} from '/js/utils.js?v=b6e6c040'; +} from '/js/utils.js?v=14b18b8c'; import { initFluidHover } from '/js/fluid-hover.js?v=cb886e86'; -import { initSpotlight } from '/js/spotlight.js?v=c39b3f63'; +import { initSpotlight } from '/js/spotlight.js?v=97143da4'; import { html, setHtml, raw } from '/js/html.js?v=c71f8903'; -import { initI18n, t, currentLang } from '/js/i18n.js?v=f754f6fe'; -import { blockingScreenFor, showBlockingScreen } from '/js/config-recovery.js?v=e22b5e5a'; +import { initI18n, t, currentLang } from '/js/i18n.js?v=ed7c5565'; +import { blockingScreenFor, showBlockingScreen } from '/js/config-recovery.js?v=85138697'; import { pwStrength, passwordMismatch } from '/js/password-strength.js?v=389e0ed0'; import { setupErrorKey } from '/js/admin-error.js?v=b0881957'; import { sanitizeItemLinks } from '/js/link-url.js?v=54adb40f'; @@ -46,10 +46,10 @@ import { buildMobile, resetMobileChrome, mkFolderGlyph, -} from '/js/ui.js?v=fd480686'; +} from '/js/ui.js?v=6af083c8'; import { badgeMinimum, badgeSignature, computeBadgeVisual, readBadgeUpdate } from '/js/badge-logic.js?v=5c72bd4c'; -import { formatNumber } from '/js/format-number.js?v=52226e9c'; -import { closeBadgePopover, wireBadgePopover } from '/js/badge-popover.js?v=9f510a4c'; +import { formatNumber } from '/js/format-number.js?v=abcd4ae4'; +import { closeBadgePopover, wireBadgePopover } from '/js/badge-popover.js?v=23793736'; import { observeGlass } from '/js/glass-rim.js?v=3faec233'; import { configChanged, @@ -59,7 +59,7 @@ import { restorePage, wheelPager, } from '/js/dashboard-logic.js?v=6a29cedc'; -import { applyBackground, BACKDROP, resolveBackground } from '/js/background.js?v=1bd5f25e'; +import { applyBackground, BACKDROP, resolveBackground } from '/js/background.js?v=0f4ca726'; import { repeatJittered } from '/js/jitter.js?v=087a1fcf'; import { isMobileLayout, onLayoutChange } from '/js/layout.js?v=e9f4b607'; import { startWakeLock } from '/js/wake-lock.js?v=6b9591cf'; diff --git a/ui/js/format-number.js b/ui/js/format-number.js index 16605662..64cb5710 100644 --- a/ui/js/format-number.js +++ b/ui/js/format-number.js @@ -60,12 +60,15 @@ export function formatNumber(value, options) { } /** Join items the way the interface language writes a list. - @param {string[]} items @param {'conjunction'|'unit'} [type] + @param {string[]} items @param {'conjunction'|'plain'} [type] @returns {string} */ export function formatList(items, type = 'conjunction') { const list = items.map(String); try { - return new Intl.ListFormat(language || 'en', { type, style: type === 'unit' ? 'short' : 'long' }).format(list); + return new Intl.ListFormat(language || 'en', { + type: 'conjunction', + style: type === 'plain' ? 'narrow' : 'long', + }).format(list); } catch { return list.join(', '); } diff --git a/ui/js/i18n.js b/ui/js/i18n.js index e11eaa6f..23fc25d6 100644 --- a/ui/js/i18n.js +++ b/ui/js/i18n.js @@ -4,7 +4,7 @@ import { setHtml } from '/js/html.js?v=c71f8903'; import { i18nMarkup } from '/js/i18n-markup.js?v=8c90e1dd'; -import { formatNumber, setNumberLanguage } from '/js/format-number.js?v=52226e9c'; +import { formatNumber, setNumberLanguage } from '/js/format-number.js?v=abcd4ae4'; /* The locale registry: the one place a supported language is defined. diff --git a/ui/js/listbox.js b/ui/js/listbox.js index b0db0513..b9368457 100644 --- a/ui/js/listbox.js +++ b/ui/js/listbox.js @@ -8,9 +8,9 @@ import { nextActiveIndex } from '/js/admin-logic.js?v=fc7f0836'; import { fluidHoverClear, fluidHoverKb } from '/js/fluid-hover.js?v=cb886e86'; -import { formatList } from '/js/format-number.js?v=52226e9c'; +import { formatList } from '/js/format-number.js?v=abcd4ae4'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; -import { qa } from '/js/utils.js?v=b6e6c040'; +import { qa } from '/js/utils.js?v=14b18b8c'; import { iconSvg } from '/js/icon-set.js?v=628341d4'; const CHEV = iconSvg('chevrons', 22, 'solid', 'dd-chev'); @@ -118,7 +118,7 @@ export function createListbox( else text.textContent = formatList( sel.map(o => o.label), - 'unit', + 'plain', ); text.classList.toggle('is-ph', !sel.length && !summary); const lang = !summary && sel.length === 1 ? sel[0].lang : ''; diff --git a/ui/js/spotlight.js b/ui/js/spotlight.js index 59cc4bef..c5e87350 100644 --- a/ui/js/spotlight.js +++ b/ui/js/spotlight.js @@ -1,6 +1,6 @@ import { fluidHoverClear, fluidHoverKb } from '/js/fluid-hover.js?v=cb886e86'; -import { mk, clr, el, initial, inp as inpById, q, qa, setUserText } from '/js/utils.js?v=b6e6c040'; -import { t } from '/js/i18n.js?v=f754f6fe'; +import { mk, clr, el, initial, inp as inpById, q, qa, setUserText } from '/js/utils.js?v=14b18b8c'; +import { t } from '/js/i18n.js?v=ed7c5565'; /* Attached to the window so a re-open can undo the previous one. */ const _w = /** @type {any} */ (window); diff --git a/ui/js/ui.js b/ui/js/ui.js index 5feea029..b8b31395 100644 --- a/ui/js/ui.js +++ b/ui/js/ui.js @@ -16,8 +16,8 @@ import { q, qa, setUserText, -} from '/js/utils.js?v=b6e6c040'; -import { t, currentLang } from '/js/i18n.js?v=f754f6fe'; +} from '/js/utils.js?v=14b18b8c'; +import { t, currentLang } from '/js/i18n.js?v=ed7c5565'; import { toneForColor } from '/js/label-contrast.js?v=0b1ebb19'; import { mobileMetrics, gridColumnWidth, gridCellCount } from '/js/mobile-metrics.js?v=349f4300'; import { smoothRectPath } from '/js/smooth-corner.js?v=b7dda7e1'; diff --git a/ui/js/utils.js b/ui/js/utils.js index de489029..03e99144 100644 --- a/ui/js/utils.js +++ b/ui/js/utils.js @@ -3,7 +3,7 @@ import { toneForColor } from '/js/label-contrast.js?v=0b1ebb19'; import { SETTINGS_ICON, SETTINGS_ICON_LIGHT } from '/js/settings-icon.js?v=4079b66a'; import { mkGlassRim } from '/js/glass-rim.js?v=3faec233'; import { smoothRectPath } from '/js/smooth-corner.js?v=b7dda7e1'; -import { t } from '/js/i18n.js?v=f754f6fe'; +import { t } from '/js/i18n.js?v=ed7c5565'; import { pageTheme, tileColor } from '/js/palette.js?v=3fb8ae43'; export const mk = (tag, a = {}) => { diff --git a/ui/js/widget-config-form.js b/ui/js/widget-config-form.js index 62153f58..a79a23ab 100644 --- a/ui/js/widget-config-form.js +++ b/ui/js/widget-config-form.js @@ -1,11 +1,11 @@ /* Renders a widget's declared `fields` into a config form and reads the values back. Each builder returns { el, get, control, liveValue }. */ -import { t } from '/js/i18n.js?v=f754f6fe'; +import { t } from '/js/i18n.js?v=ed7c5565'; import { html, raw, setHtml } from '/js/html.js?v=c71f8903'; -import { reveal, wireInlineEdit } from '/js/admin-shared.js?v=38ed2b51'; -import { createListbox } from '/js/listbox.js?v=ae7b9a38'; -import { renderColorControl } from '/js/admin-color-control.js?v=44fdebde'; +import { reveal, wireInlineEdit } from '/js/admin-shared.js?v=36dc5cc1'; +import { createListbox } from '/js/listbox.js?v=23e87940'; +import { renderColorControl } from '/js/admin-color-control.js?v=e80f4a68'; import { seedCarried, applyOptionSet, @@ -15,8 +15,8 @@ import { visibleFieldFlags, } from '/js/admin-logic.js?v=fc7f0836'; import { optionsErrorAdvice, TONE } from '/js/admin-error.js?v=b0881957'; -import { qi } from '/js/utils.js?v=b6e6c040'; -import { formatNumber } from '/js/format-number.js?v=52226e9c'; +import { qi } from '/js/utils.js?v=14b18b8c'; +import { formatNumber } from '/js/format-number.js?v=abcd4ae4'; const PE = ''; diff --git a/ui/js/widget-toolbox.js b/ui/js/widget-toolbox.js index 28e13ce8..e7d226dd 100644 --- a/ui/js/widget-toolbox.js +++ b/ui/js/widget-toolbox.js @@ -5,7 +5,7 @@ import { esc, html, setHtml } from '/js/html.js?v=c71f8903'; import { isSafeLinkUrl } from '/js/link-url.js?v=54adb40f'; import { jitter } from '/js/jitter.js?v=087a1fcf'; import { errorState as _errorState, errorKind, errorCopy } from '/js/widget-error.js?v=3bfb691c'; -import { formatNumber, setNumberLanguage } from '/js/format-number.js?v=52226e9c'; +import { formatNumber, setNumberLanguage } from '/js/format-number.js?v=abcd4ae4'; export { esc, html, setHtml }; @@ -482,7 +482,7 @@ export function wtCount(key, count, fallback, vars) { /* Digit shape follows the interface language. Re-exported here so a widget takes it from the toolbox rather than reaching for toLocaleString, which is the same thing until someone passes it a language. */ -export { formatNumber, formatList, localiseDigits } from '/js/format-number.js?v=52226e9c'; +export { formatNumber, formatList, localiseDigits } from '/js/format-number.js?v=abcd4ae4'; export function sinceLabel(ts) { if (!ts) return ''; diff --git a/ui/test/localised-digits.test.mjs b/ui/test/localised-digits.test.mjs index 16b333e9..5f9f2519 100644 --- a/ui/test/localised-digits.test.mjs +++ b/ui/test/localised-digits.test.mjs @@ -392,7 +392,7 @@ test('a list is joined in the interface language', async () => { formatList(['1', '2', '3']), new Intl.ListFormat('fa', { type: 'conjunction' }).format(['1', '2', '3']), ); - assert.match(formatList(['a', 'b'], 'unit'), /،/); + assert.match(formatList(['a', 'b'], 'plain'), /،/); }, 'fa', ); @@ -401,3 +401,21 @@ test('a list is joined in the interface language', async () => { }); await withLocale('en-US', ({ formatList }) => assert.equal(formatList(['a', 'b']), 'a, b'), 'not a tag'); }); + +test('a plain list keeps a separator in every interface language', async () => { + await withLocale('en-US', ({ formatList }) => assert.equal(formatList(['a', 'b', 'c'], 'plain'), 'a, b, c')); + await withLocale( + 'en-US', + ({ formatList }) => assert.equal(formatList(['甲', '乙', '丙'], 'plain'), '甲、乙、丙'), + 'zh-Hans', + ); +}); + +test('no widget joins a visible or spoken list with a hard-coded comma', () => { + for (const file of [ + 'widgets/github/pullrequests.html', + 'widgets/connections/connections-map.html', + 'widgets/system-summary/index.html', + ]) + assert.doesNotMatch(read(file), /\.join\(', '\)|type: 'unit'/, file); +}); diff --git a/ui/widgets/backup/backup.html b/ui/widgets/backup/backup.html index 22963876..7dab7323 100644 --- a/ui/widgets/backup/backup.html +++ b/ui/widgets/backup/backup.html @@ -122,7 +122,7 @@