fix(i18n): drop spaced dashes as separators from every visible string

A dash between two parts of a sentence ("{what} — tomorrow", "Could not
record {name} — try again", "End {name} — …") stands in for a comma, a
colon or a full stop the sentence should say. Every such string in the
seven bundles now says it in plain words. A range keeps its unspaced en
dash ("70–100 mmHg", "{start}–{end}"); that is how a range is written,
not a pause, and the spaced range forms became unspaced ones.

Labels the components put together themselves follow the same rule:
aria-labels read "Move up: Ramipril", a medication and its dose read
"Ramipril 5 mg", page titles use "|", Coach prefills are two sentences,
and the API's user-facing error messages lost their dashes too.

The bundle guard now refuses " — " and " – " beside " · ", with an empty
allowlist, and a sibling guard walks src/components and src/app for a
string or template literal that joins its parts with a spaced dash.
This commit is contained in:
Marc-André Bombeck committed 2026-10-09 16:05:32 +02:00
1 parent 1d080ee166
commit 86a3856ae9
107 files changed
+2485 -2412

No files matched your search

@@ -72,7 +72,7 @@ describe("chart isError rendering — error state, not empty state", () => {
expect(html).toContain("Pulse");
// The retry button carries the chart title in its accessible name
// so several failed charts on one page stay distinguishable.
expect(html).toContain('aria-label="Erneut versuchen – Pulse"');
expect(html).toContain('aria-label="Erneut versuchen: Pulse"');
// The empty-state copy must NOT paint — an outage is not "no data".
expect(html).not.toContain("Keine Daten in diesem Zeitraum");
@@ -169,7 +169,7 @@ describe("<ChartErrorState> — announcement, sizing, accessible name", () => {
it("joins the action context into the retry button's accessible name", async () => {
const html = await renderState({ actionContext: "Pulse" });
expect(html).toContain('aria-label="Retry – Pulse"');
expect(html).toContain('aria-label="Retry: Pulse"');
});
it("omits the aria-label without a context so the label is the name", async () => {
+1 -1
View File
@@ -82,7 +82,7 @@ export function ChartErrorState({
size="sm"
onClick={onAction}
aria-label={
actionContext ? `${actionLabel} – ${actionContext}` : undefined
actionContext ? `${actionLabel}: ${actionContext}` : undefined
}
>
<RotateCw className="h-3.5 w-3.5" aria-hidden="true" />
@@ -152,7 +152,7 @@ export function IllnessSection({
: null,
t(`illness.type.${episode.type}`),
t(`illness.lifecycle.${episode.lifecycle}`),
`${fmtDate(episode.onsetAt)} – ${
`${fmtDate(episode.onsetAt)}–${
episode.resolvedAt
? fmtDate(episode.resolvedAt)
: t("doctorReport.illnessOngoing")
+1 -1
View File
@@ -357,7 +357,7 @@ export function MedicationsSection({
return (
<Fragment key={med.id}>
<StatRow
label={med.dose ? `${med.name} — ${med.dose}` : med.name}
label={med.dose ? `${med.name} ${med.dose}` : med.name}
value={
rate
? t("clinicianView.adherence", { rate })
@@ -206,7 +206,7 @@ export function DoseLogSection({
{shown.map((dose, index) => (
<StatRow
key={`${dose.medicationName}-${dose.effectiveAt}-${index}`}
label={`${fmtDateTime(dose.effectiveAt)} — ${dose.medicationName}`}
label={`${fmtDateTime(dose.effectiveAt)}: ${dose.medicationName}`}
value={
(dose.status === "completed"
? t("doctorReport.colTaken")
+2 -2
View File
@@ -99,7 +99,7 @@ export function PredictionsPanel({
<Row
hue={FERTILE_HUE}
label={t("cycle.predictions.fertileWindow")}
value={`${formatDate(prediction.fertileWindowStart, fmt)} – ${formatDate(
value={`${formatDate(prediction.fertileWindowStart, fmt)}–${formatDate(
prediction.fertileWindowEnd,
fmt,
)}`}
@@ -264,7 +264,7 @@ function HistoryCard({
>
<span className="text-foreground tabular-nums">
{formatDate(c.startDate, fmt)}
{" – "}
{"–"}
{c.endDate
? formatDate(c.endDate, fmt)
: t("cycle.history.ongoing")}
@@ -125,7 +125,7 @@ export function AchievementUnlockNotifier({
// card) so the surprise lands.
if (achievement.isHidden) {
toast(t("achievements.hiddenUnlockToast.title"), {
description: `${t(achievement.titleKey)} — ${t(achievement.descriptionKey)}`,
description: `${t(achievement.titleKey)}: ${t(achievement.descriptionKey)}`,
icon: <Sparkles className="size-4" />,
duration: 8000,
});
@@ -194,7 +194,7 @@ describe("<CoincidentDeviationCard>", () => {
expect(html).toContain('data-state="fired"');
expect(html).toContain('data-slot="coincident-factors"');
// The load-bearing framing line.
expect(html).toContain("Possible factors — never a cause");
expect(html).toContain("Possible factors (never a cause)");
expect(html).toContain("not a diagnosis");
// At most amber — never destructive/red.
expect(html).toContain("border-warning");
@@ -226,7 +226,7 @@ describe("<CoincidentDeviationCard>", () => {
expect(html).toContain('data-state="fired"');
// The reframe line replaces the open-ended "possible factors" copy.
expect(html).toContain("the condition you logged");
expect(html).not.toContain("Possible factors — never a cause");
expect(html).not.toContain("Possible factors (never a cause)");
// Annotate, don't escalate: no new destructive colour beyond the amber the
// fired state already carries.
expect(html).not.toContain("text-destructive");
@@ -282,8 +282,8 @@ describe("<MetricTargetSummary>", () => {
expect(html).toContain("Source: ADA 2024 / DDG");
// The i18n label keys resolve to EN context headings (rendered
// upper-cased by the heading style but kept as text content).
expect(html).toContain("Glucose — fasting");
expect(html).toContain("Glucose — post-meal");
expect(html).toContain("Glucose (fasting)");
expect(html).toContain("Glucose (post-meal)");
});
it("converts the glucose bands to mmol/L when the profile prefers it", () => {
@@ -476,7 +476,7 @@ function SortableSectionRow({
type="button"
{...attributes}
{...listeners}
aria-label={`${labels.dragHandle} — ${title}`}
aria-label={`${labels.dragHandle}: ${title}`}
title={labels.dragHandle}
disabled={disabled}
data-slot="insights-edit-section-handle"
@@ -507,7 +507,7 @@ function SortableSectionRow({
<EyeToggle
visible={section.visible}
disabled={disabled || gatedOff}
label={`${section.visible ? labels.hide : labels.show} — ${title}`}
label={`${section.visible ? labels.hide : labels.show}: ${title}`}
onClick={() => onToggle(section.id, !section.visible)}
slot="insights-edit-section-eye"
/>
@@ -316,7 +316,7 @@ export function MetricStatStrip({
role="group"
aria-label={
seriesLabel
? `${t("insights.subPage.stats.label")} — ${seriesLabel}`
? `${t("insights.subPage.stats.label")}: ${seriesLabel}`
: t("insights.subPage.stats.label")
}
className="gap-2 py-3 md:py-4"
@@ -140,5 +140,5 @@ export function formatDateRange(
minute: "2-digit",
...hourCycleOptions(timeFormat),
});
return `${dateFmt.format(start)} · ${timeFmt.format(start)} – ${timeFmt.format(end)}`;
return `${dateFmt.format(start)}, ${timeFmt.format(start)}–${timeFmt.format(end)}`;
}
@@ -54,14 +54,14 @@ describe("<OfflineBanner>", () => {
// the honest thing.
const en = loadMessages("en");
expect(en.offlineBanner.message).toBe(
"No connection — changes can't be saved until you're back online.",
"No connection: changes can't be saved until you're back online.",
);
});
it("ships the DE copy for `offlineBanner.message`", () => {
const de = loadMessages("de");
expect(de.offlineBanner.message).toBe(
"Keine Verbindung — Änderungen können erst gespeichert werden, wenn du wieder online bist.",
"Keine Verbindung: Änderungen können erst gespeichert werden, wenn du wieder online bist.",
);
});
@@ -105,7 +105,7 @@ describe("<InjectionSitePicker>", () => {
// Exactly one site is recommended → exactly one composed label.
const composed =
html.match(/aria-label="[^"]*— recommended next site"/g) ?? [];
html.match(/aria-label="[^"]*: recommended next site"/g) ?? [];
expect(composed.length).toBe(1);
});
@@ -153,7 +153,7 @@ describe("<InjectionSitePicker>", () => {
// empty history the recommender lands on ABDOMEN_LEFT, so its label
// folds in the "recommended next site" cue.
expect(html).toContain(
'aria-label="Abdomen, lower left — recommended next site"',
'aria-label="Abdomen, lower left: recommended next site"',
);
expect(html).toContain('aria-label="Abdomen, lower right"');
expect(html).toContain('aria-label="Abdomen, upper left"');
@@ -173,7 +173,7 @@ describe("<InjectionSitePicker>", () => {
);
expect(html).toContain(
'aria-label="Bauch, unten links — empfohlene nächste Stelle"',
'aria-label="Bauch, unten links: empfohlene nächste Stelle"',
);
expect(html).toContain('aria-label="Bauch, unten rechts"');
expect(html).toContain('aria-label="Bauch, oben links"');
@@ -7,7 +7,7 @@
* - every column renders from the SAME payloads the cards consume
* (the list row's `nextDueAt` / `stockDosesRemaining`, the batched
* compliance-summary cache entry the cards share);
* - the action buttons are per-row labelled ("Take – {name}") and the
* - the action buttons are per-row labelled ("Take {name}") and the
* row rides the cards' shared intake hook (source-level guard);
* - tri-state sorting semantics incl. `aria-sort` and null-last
* ordering, with `null` sort = the manual order the page passed in;
@@ -254,8 +254,8 @@ describe("<MedicationTable> — structure + shared payloads", () => {
client,
);
expect(html).toContain('aria-label="Take – Ramipril"');
expect(html).toContain('aria-label="Skip – Ramipril"');
expect(html).toContain('aria-label="Take Ramipril"');
expect(html).toContain('aria-label="Skip Ramipril"');
// 44-px mobile floor on the action buttons (shrinks at sm).
expect(html).toContain("size-11 sm:size-9");
});
@@ -345,7 +345,7 @@ describe("<MedicationTable> — structure + shared payloads", () => {
expect(html).toContain("opacity-60");
expect(html).toContain("Inactive");
// No take/skip affordance on the inactive row.
expect(html).not.toContain('aria-label="Take – Amoxicillin"');
expect(html).not.toContain('aria-label="Take Amoxicillin"');
});
// v1.29.x — the Today digest's "Log dose" deep-link
@@ -407,7 +407,7 @@ describe("<MedicationTable> — structure + shared payloads", () => {
);
expect(html).toContain("Mounjaro");
expect(html).toContain('aria-label="Take – Mounjaro"');
expect(html).toContain('aria-label="Take Mounjaro"');
expect(html).toContain("4 doses");
});
@@ -436,12 +436,12 @@ describe("<MedicationTable> — structure + shared payloads", () => {
client,
);
expect(html).not.toContain('aria-label="Take – Amoxicillin"');
expect(html).not.toContain('aria-label="Skip – Amoxicillin"');
expect(html).not.toContain('aria-label="Take Amoxicillin"');
expect(html).not.toContain('aria-label="Skip Amoxicillin"');
expect(html).toContain('data-slot="medication-table-course-ended-marker"');
expect(html).toContain(">Ended<");
expect(html).toContain('aria-label="Take – Prednisolone"');
expect(html).toContain('aria-label="Skip – Prednisolone"');
expect(html).toContain('aria-label="Take Prednisolone"');
expect(html).toContain('aria-label="Skip Prednisolone"');
});
it("renders an as-needed row: calm marker, no compliance bars (v1.16.11 #316)", () => {
@@ -476,7 +476,7 @@ describe("<MedicationTable> — structure + shared payloads", () => {
// Compliance column shows the en-dash, not bars or a skeleton.
expect(html).not.toContain("Adherence (");
// The quick actions stay — an as-needed dose is loggable inline.
expect(html).toContain('aria-label="Take – Ibuprofen"');
expect(html).toContain('aria-label="Take Ibuprofen"');
// Stock column still renders (inventory tracking works for PRN).
expect(html).toContain("10 doses");
});
@@ -292,7 +292,7 @@ export function LogIntakeDialog({
>
{medications.map((m) => (
<option key={m.id} value={m.id}>
{m.dose ? `${m.name} — ${formatDose(m.dose, t)}` : m.name}
{m.dose ? `${m.name} ${formatDose(m.dose, t)}` : m.name}
</option>
))}
</NativeSelect>
@@ -355,7 +355,7 @@ function SortableMedicationRow({
type="button"
{...attributes}
{...listeners}
aria-label={`${labels.dragHandle} — ${medication.name}`}
aria-label={`${labels.dragHandle}: ${medication.name}`}
aria-describedby={dragHintId}
title={labels.dragHandle}
disabled={disabled}
@@ -380,7 +380,7 @@ function SortableMedicationRow({
className="size-11 sm:size-9"
onClick={() => onMove(medication.id, -1)}
disabled={index === 0 || disabled}
aria-label={`${labels.moveUp} — ${medication.name}`}
aria-label={`${labels.moveUp}: ${medication.name}`}
>
<ArrowUp className="h-4 w-4" />
</Button>
@@ -391,7 +391,7 @@ function SortableMedicationRow({
className="size-11 sm:size-9"
onClick={() => onMove(medication.id, 1)}
disabled={index === total - 1 || disabled}
aria-label={`${labels.moveDown} — ${medication.name}`}
aria-label={`${labels.moveDown}: ${medication.name}`}
>
<ArrowDown className="h-4 w-4" />
</Button>
@@ -216,7 +216,7 @@ export function ScheduleHistoryTimeline({
local day (issue #490 audit). */}
<p className="text-muted-foreground text-xs tabular-nums">
{fmt.date(revision.validFrom)}
{" – "}
{"–"}
{fmt.date(revision.validUntil)}
</p>
</div>
@@ -132,7 +132,7 @@ export function InstrumentCard({
type="button"
onClick={onOpenDetail}
data-slot="instrument-card-open"
aria-label={`${title} — ${t("mentalHealth.openDetail")}`}
aria-label={`${title}: ${t("mentalHealth.openDetail")}`}
className="focus-visible:ring-ring block w-full rounded-t-xl text-left focus-visible:ring-2 focus-visible:outline-none"
>
<MedicationCardHeader
@@ -180,7 +180,7 @@ function SortableRow({
type="button"
{...attributes}
{...listeners}
aria-label={`${labels.dragHandle} — ${item.name}`}
aria-label={`${labels.dragHandle}: ${item.name}`}
aria-describedby={dragHintId}
title={labels.dragHandle}
data-slot="module-drag-handle"
@@ -205,7 +205,7 @@ function SortableRow({
className="size-11 sm:size-9"
onClick={() => onMove(item.id, -1)}
disabled={index === 0}
aria-label={`${labels.moveUp} — ${item.name}`}
aria-label={`${labels.moveUp}: ${item.name}`}
>
<ArrowUp className="h-4 w-4" />
</Button>
@@ -216,7 +216,7 @@ function SortableRow({
className="size-11 sm:size-9"
onClick={() => onMove(item.id, 1)}
disabled={index === total - 1}
aria-label={`${labels.moveDown} — ${item.name}`}
aria-label={`${labels.moveDown}: ${item.name}`}
>
<ArrowDown className="h-4 w-4" />
</Button>
@@ -153,7 +153,7 @@ export function ArchivedTagsCard({ catalog }: ArchivedTagsCardProps) {
size="icon"
className="text-muted-foreground hover:text-foreground size-11 sm:size-9"
onClick={() => setPurgeTag(tag)}
aria-label={`${t("mood.manage.purgeAction")} — ${name}`}
aria-label={`${t("mood.manage.purgeAction")}: ${name}`}
title={t("mood.manage.purgeAction")}
>
<Trash2 className="h-4 w-4" aria-hidden="true" />
@@ -432,7 +432,7 @@ function SortableGroupRow({
type="button"
{...attributes}
{...listeners}
aria-label={`${t("dashboard.dragHandle")} — ${name}`}
aria-label={`${t("dashboard.dragHandle")}: ${name}`}
aria-describedby={dragHintId}
title={t("dashboard.dragHandle")}
disabled={disabled}
@@ -461,7 +461,7 @@ function SortableGroupRow({
className="size-11 sm:size-9"
onClick={() => onMove(groupKey, -1)}
disabled={index === 0 || disabled}
aria-label={`${t("mood.manage.moveUp")} — ${name}`}
aria-label={`${t("mood.manage.moveUp")}: ${name}`}
>
<ArrowUp className="h-4 w-4" />
</Button>
@@ -472,7 +472,7 @@ function SortableGroupRow({
className="size-11 sm:size-9"
onClick={() => onMove(groupKey, 1)}
disabled={index === total - 1 || disabled}
aria-label={`${t("mood.manage.moveDown")} — ${name}`}
aria-label={`${t("mood.manage.moveDown")}: ${name}`}
>
<ArrowDown className="h-4 w-4" />
</Button>
@@ -484,7 +484,7 @@ function SortableGroupRow({
variant="ghost"
size="icon"
className="size-11 sm:size-9"
aria-label={`${t("common.moreOptions")} — ${name}`}
aria-label={`${t("common.moreOptions")}: ${name}`}
>
<MoreHorizontal className="h-4 w-4" />
</Button>
@@ -546,7 +546,7 @@ function SortableTagRow({
type="button"
{...attributes}
{...listeners}
aria-label={`${t("dashboard.dragHandle")} — ${name}`}
aria-label={`${t("dashboard.dragHandle")}: ${name}`}
aria-describedby={dragHintId}
title={t("dashboard.dragHandle")}
disabled={disabled}
@@ -589,7 +589,7 @@ function SortableTagRow({
: isHidden
? t("mood.manage.showTag")
: t("mood.manage.hideTag")
} — ${name}`}
}: ${name}`}
title={
tag.custom
? t("mood.manage.archive")
@@ -613,7 +613,7 @@ function SortableTagRow({
className="size-11 sm:size-9"
onClick={() => onMove(groupKey, tag.key, -1)}
disabled={index === 0 || disabled}
aria-label={`${t("mood.manage.moveUp")} — ${name}`}
aria-label={`${t("mood.manage.moveUp")}: ${name}`}
>
<ArrowUp className="h-4 w-4" />
</Button>
@@ -624,7 +624,7 @@ function SortableTagRow({
className="size-11 sm:size-9"
onClick={() => onMove(groupKey, tag.key, 1)}
disabled={index === total - 1 || disabled}
aria-label={`${t("mood.manage.moveDown")} — ${name}`}
aria-label={`${t("mood.manage.moveDown")}: ${name}`}
>
<ArrowDown className="h-4 w-4" />
</Button>
@@ -635,7 +635,7 @@ function SortableTagRow({
variant="ghost"
size="icon"
className="size-11 sm:size-9"
aria-label={`${t("common.moreOptions")} — ${name}`}
aria-label={`${t("common.moreOptions")}: ${name}`}
>
<MoreHorizontal className="h-4 w-4" />
</Button>
@@ -336,8 +336,8 @@ describe("<DashboardLayoutSection> — native-only widget group (issue #581)", (
const html = render(<DashboardLayoutSection id="dashboard-layout" />);
expect(html).toContain('data-widget-id="bmi"');
expect(html).toContain('aria-label="BMI — Tile"');
expect(html).toContain('aria-label="BMI — Chart"');
expect(html).toContain('aria-label="BMI: Tile"');
expect(html).toContain('aria-label="BMI: Chart"');
// Reflects the stored flags rather than inventing a default.
const row = html.match(
/data-widget-id="bmi"[\s\S]*?(?=data-slot="native-widget-row"|$)/,
@@ -448,7 +448,7 @@ describe("<DashboardLayoutSection> — iOS-pin-only ids hidden from web (v1.11.2
html.indexOf('data-slot="native-only-widgets"'),
);
for (const id of IOS_PIN_ONLY_WIDGET_IDS) {
expect(sortableList).not.toContain(`${pinOnlyLabels[id]} — `);
expect(sortableList).not.toContain(`${pinOnlyLabels[id]}: `);
// ...and each is reachable exactly once, in the native-only group.
expect(html).toContain(`data-widget-id="${id}"`);
}
@@ -465,11 +465,11 @@ describe("<DashboardLayoutSection> — iOS-pin-only ids hidden from web (v1.11.2
describe("<DashboardLayoutSection> — disabled-module widget toggles", () => {
// The achievements widget label drives its switch aria-label; it is the
// canonical disabled-module probe (label resolves to "Achievements").
const ACHIEVEMENTS_ARIA = "Achievements — ";
const ACHIEVEMENTS_ARIA = "Achievements: ";
// Mood is a module-owned widget too; weight is a core widget with NO module
// entry, so it must survive any module-off state.
const MOOD_ARIA = "Mood — ";
const WEIGHT_ARIA = "Weight — ";
const MOOD_ARIA = "Mood: ";
const WEIGHT_ARIA = "Weight: ";
it("hides a widget toggle whose owning module is disabled", () => {
authState.modules = { achievements: false };
@@ -105,7 +105,7 @@ describe("IntegrationStatusPill", () => {
<IntegrationStatusPill state="parked" lastSyncAt={null} />,
);
expect(html).toContain("Paused");
expect(html).toContain("Paused — reconnect");
expect(html).toContain("Paused: reconnect");
expect(html).toContain('data-state="parked"');
});
@@ -115,7 +115,7 @@ describe("IntegrationStatusPill", () => {
"de",
);
expect(html).toContain("Pausiert");
expect(html).toContain("Pausiert — neu verbinden");
expect(html).toContain("Pausiert: neu verbinden");
});
// The three states the server verdict added. `stale` and `stalled` both
@@ -233,7 +233,7 @@ describe("IntegrationsSection — single-status-display contract (A5)", () => {
// The pill flips to `data-state="parked"` and the resume banner
// renders with the reconnect CTA. The legacy `error` reconnect text
// (red) must NOT appear because the user-facing copy is the lighter
// "Paused — reconnect" string, not "Error — reconnect".
// "Paused: reconnect" string, not "Error — reconnect".
it("Withings parked state surfaces the parked pill + resume CTA", () => {
setIntegrationStatus({
threshold: 3,
@@ -256,7 +256,7 @@ describe("IntegrationsSection — single-status-display contract (A5)", () => {
const html = render();
expect(html).toContain('data-state="parked"');
expect(html).toContain("Paused");
expect(html).toContain("Paused — reconnect");
expect(html).toContain("Paused: reconnect");
expect(html).toContain('data-testid="withings-parked-banner"');
expect(html).toContain('data-testid="withings-resume-button"');
// The lastError is surfaced under the pill so the operator sees
@@ -190,9 +190,9 @@ describe("<MoodSection> — SSR smoke", () => {
const groupRows = html.match(/data-slot="mood-group-row"/g);
expect(groupRows?.length ?? 0).toBe(2);
// One kebab per CUSTOM group row; the seeded row carries none.
const kebabs = html.match(/More options — Garten/g);
const kebabs = html.match(/More options: Garten/g);
expect(kebabs?.length ?? 0).toBeGreaterThanOrEqual(1);
expect(html).not.toContain("More options — Feelings");
expect(html).not.toContain("More options: Feelings");
});
it("renders custom tags by decrypted label, never the raw key", () => {
@@ -206,7 +206,7 @@ describe("<MoodSection> — SSR smoke", () => {
expect(html).toMatch(
/data-slot="mood-tag-manage-row"[^>]*data-hidden="true"/,
);
expect(html).toContain("Show tag — stressed");
expect(html).toContain("Show tag: stressed");
});
it("surfaces the usage count badge", () => {
@@ -219,7 +219,7 @@ describe("<MoodSection> — SSR smoke", () => {
expect(html).toContain('data-slot="mood-archived-row"');
expect(html).toContain("Altes Tag");
expect(html).toContain("Restore");
expect(html).toContain("Delete permanently — Altes Tag");
expect(html).toContain("Delete permanently: Altes Tag");
// The active custom tag must NOT appear in the archived card.
const archivedCard = html.slice(
html.indexOf('data-slot="mood-archived-tags-card"'),
@@ -877,7 +877,7 @@ function SortableWidgetRow({
type="button"
{...attributes}
{...listeners}
aria-label={`${labels.dragHandle} — ${labels.widgetLabel}`}
aria-label={`${labels.dragHandle}: ${labels.widgetLabel}`}
// v1.4.47 W4 — `aria-describedby` is set after `{...attributes}`
// so our shared hint paragraph wins over dnd-kit's own announcer
// hookup. The announcer still fires on drag-start / drag-over /
@@ -907,7 +907,7 @@ function SortableWidgetRow({
<Switch
checked={tileChecked}
onCheckedChange={(v) => onToggleTile(widget.id, v)}
aria-label={`${labels.widgetLabel} — ${labels.tileColumn}`}
aria-label={`${labels.widgetLabel}: ${labels.tileColumn}`}
disabled={disabled}
data-slot="widget-tile-switch"
/>
@@ -916,7 +916,7 @@ function SortableWidgetRow({
<Switch
checked={widget.visible}
onCheckedChange={(v) => onToggleChart(widget.id, v)}
aria-label={`${labels.widgetLabel} — ${labels.chartColumn}`}
aria-label={`${labels.widgetLabel}: ${labels.chartColumn}`}
disabled={disabled}
data-slot="widget-chart-switch"
/>
@@ -989,7 +989,7 @@ function StaticWidgetRow({
<Switch
checked={tileChecked}
onCheckedChange={(v) => onToggleTile(widget.id, v)}
aria-label={`${labels.widgetLabel} — ${labels.tileColumn}`}
aria-label={`${labels.widgetLabel}: ${labels.tileColumn}`}
disabled={disabled}
data-slot="widget-tile-switch"
/>
@@ -998,7 +998,7 @@ function StaticWidgetRow({
<Switch
checked={widget.visible}
onCheckedChange={(v) => onToggleChart(widget.id, v)}
aria-label={`${labels.widgetLabel} — ${labels.chartColumn}`}
aria-label={`${labels.widgetLabel}: ${labels.chartColumn}`}
disabled={disabled}
data-slot="widget-chart-switch"
/>
@@ -402,7 +402,7 @@ function EcgManagerRow({
onClick={onToggle}
disabled={disabled}
aria-pressed={visible}
aria-label={`${visible ? hideLabel : showLabel} — ${title}`}
aria-label={`${visible ? hideLabel : showLabel}: ${title}`}
title={visible ? hideLabel : showLabel}
data-slot="insights-pill-order-eye"
data-visible={visible ? "true" : "false"}
@@ -463,7 +463,7 @@ function SortablePillRow({
type="button"
{...attributes}
{...listeners}
aria-label={`${dragHandleLabel} — ${title}`}
aria-label={`${dragHandleLabel}: ${title}`}
title={dragHandleLabel}
disabled={disabled}
data-slot="insights-pill-order-handle"
@@ -481,7 +481,7 @@ function SortablePillRow({
onClick={() => onToggle(id, !visible)}
disabled={disabled}
aria-pressed={visible}
aria-label={`${visible ? hideLabel : showLabel} — ${title}`}
aria-label={`${visible ? hideLabel : showLabel}: ${title}`}
title={visible ? hideLabel : showLabel}
data-slot="insights-pill-order-eye"
data-visible={visible ? "true" : "false"}
@@ -63,7 +63,7 @@ describe("OAuthProviderCard — parked + test + data-link parity", () => {
expect(html).toContain('data-state="parked"');
expect(html).toContain('data-testid="polar-parked-banner"');
expect(html).toContain('data-testid="polar-resume-button"');
expect(html).toContain("Paused — reconnect");
expect(html).toContain("Paused: reconnect");
// The parked banner uses the same warning treatment as the WHOOP card.
expect(html).toContain("border-warning/30 bg-warning/10");
});
@@ -157,7 +157,7 @@ describe("OAuthProviderCard — per-user BYO credentials form (v1.17.1)", () =>
syncHealth: { verdict: "pending_first_sync", since: null },
},
});
expect(html).toContain("Saved — enter new to replace");
expect(html).toContain("Saved. Enter new to replace");
});
});
+2 -2
View File
@@ -229,7 +229,7 @@ export function FilterBarDateRange({
const fmt = useFormatters();
const active = from !== "" || to !== "";
const display = active
? `${from ? fmt.dateShortSmart(from) : "…"} – ${to ? fmt.dateShortSmart(to) : "…"}`
? `${from ? fmt.dateShortSmart(from) : "…"}–${to ? fmt.dateShortSmart(to) : "…"}`
: null;
const clear = () => {
onFromChange("");
@@ -326,7 +326,7 @@ export function FilterBarNumberRange({
toLabel: string;
}) {
const active = min !== "" || max !== "";
const display = active ? `${min || "…"} – ${max || "…"}` : null;
const display = active ? `${min || "…"}–${max || "…"}` : null;
const clear = () => {
onMinChange("");
onMaxChange("");