From eab30198dc3109426a100860e09bf8cc9adc2e65 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Thu, 16 Jul 2026 00:03:42 +0200 Subject: [PATCH] fix(locale): localize remaining ISO 8601 spelled-out date names (#8987) Under the ISO 8601 option, dateTimeLocale is the 'sv' sync sentinel, so any spelled-out weekday/month name rendered with currentLocale() prints in Swedish regardless of UI language. Follow-up to #9013/#9055 covering the surfaces those PRs didn't: spelled-out names now follow the UI language (isoTextLocale), while numeric dates and clock times keep currentLocale(). - scheduled-list-page: locale signal -> isoTextLocale ?? currentLocale (all 6 localeDate labels are spelled-out; fixes the reproduced 'ons, 15 juli' leak) - dialog-focus-session-edit.formatSelectedDate: weekday+month long - habit-tracker.dateRangeLabel: month short - scheduled-date-group.pipe: work-view group-header tooltip weekday - worklog (formatDayStr via mapArchiveToWorklog): day-header weekday Inlines the isoTextLocale ?? currentLocale expression (not #9055's textLocale) so this PR shares no files with #9055 and is conflict-free / mergeable in any order. Adds an ISO regression test to scheduled-date-group.pipe.spec and updates mocks so the new isoTextLocale() calls resolve. Also adds docs/handover.md. --- docs/handover.md | 146 ++++++++++++++++++ .../dialog-focus-session-edit.component.ts | 18 ++- .../habit-tracker/habit-tracker.component.ts | 6 +- .../features/worklog/worklog.service.spec.ts | 2 +- src/app/features/worklog/worklog.service.ts | 6 +- .../scheduled-list-page.component.ts | 14 +- .../pipes/scheduled-date-group.pipe.spec.ts | 18 +++ src/app/ui/pipes/scheduled-date-group.pipe.ts | 9 +- 8 files changed, 204 insertions(+), 15 deletions(-) create mode 100644 docs/handover.md diff --git a/docs/handover.md b/docs/handover.md new file mode 100644 index 0000000000..a83ffaaf18 --- /dev/null +++ b/docs/handover.md @@ -0,0 +1,146 @@ +# Handover: ISO 8601 "spelled-out date names leak Swedish" work + +_Last updated: 2026-07-16. This document hands off two related, in-flight pieces of +work to a fresh agent with no prior context._ + +## TL;DR + +Super Productivity's **ISO 8601 date-format option** stores `dateTimeLocale = 'sv'` +as a **backward-compatible sync sentinel** (it yields `YYYY-MM-DD` + a 24h clock). +Any code that renders **spelled-out** weekday/month names (e.g. `Wed`, `July`) using +that configured locale therefore prints them in **Swedish** (`ons`, `juli`), +regardless of the app's UI language. The correct behavior: **spelled-out names follow +the UI language; numeric dates and clock times keep the configured locale** (so ISO +stays `YYYY-MM-DD` and 24h is preserved). + +There are two branches: + +| Branch | PR | Scope | State | +| --- | --- | --- | --- | +| `feat/https-github-com-super-productivity-su-726a40` | **#9055** | Recurring-task cluster (start-date value, quick-setting weekday, repeat-info util, add-task-bar chips) | Pushed, browser-verified, **awaiting CI + merge** | +| `fix/iso-spelled-out-date-leaks` (**current**) | not opened yet | 5 more leak sites found during #9055 verification | Committed locally, **needs push + PR + verify** | + +## Background: the fix chain + +- **#8991 (merged, issue #8987):** localized ISO weekday labels only in the _custom_ + Schedule/Habits/Planner components, via `DateTimeFormatService.isoTextLocale()`. +- **#9013 (merged):** fixed the Material `` app-wide by overriding + `CustomDateAdapter.getDayOfWeekNames/getMonthNames/format` to swap to `isoTextLocale()`. +- **#9055 (open):** this session's first PR — the recurring-task dialog + add-task-bar. +- **This branch:** the remaining spelled-out leaks found by browser-testing #9055. + +## Key `DateTimeFormatService` API (in `src/app/core/date-time-format/`) + +- `currentLocale()` → the configured locale (the `sv` sentinel under ISO). Use for + **numeric** dates and clock times. +- `isoTextLocale()` → `string | null`. Returns the **UI language** _only when the ISO + (`sv`) option is active_, else `null`. This is the shared primitive (on master). +- `textLocale()` → `isoTextLocale() ?? currentLocale()`. **Added by #9055, NOT on master.** + It is the canonical "spelled-out names" locale. + +**Why this branch inlines `isoTextLocale() ?? currentLocale()` instead of using +`textLocale()`:** `textLocale` only exists on the #9055 branch. To keep this PR +**independent and conflict-free** with #9055 (no shared files, no `DateTimeFormatService` +change, mergeable in any order), it inlines the same expression — consistent with how +#8991/#9013 already inline `isoTextLocale` checks. After both merge, these inline +expressions _could_ be simplified to `textLocale()`, but that is optional cleanup. + +## This branch's 5 fixes (`fix/iso-spelled-out-date-leaks`, based on `master`) + +All follow the rule **spelled-out → UI language (`isoTextLocale() ?? currentLocale()`); +numeric → `currentLocale()`**: + +1. **`src/app/pages/scheduled-list-page/scheduled-list-page.component.ts`** — the `locale` + signal now prefers `isoTextLocale()`. All 6 `localeDate` usages in its template are + spelled-out (`EE, d MMM`), so this one change fixes all of them. _This is the leak that + was reproduced on screen: "ons, 15 juli" under German UI._ +2. **`.../metric/dialog-focus-session-edit/dialog-focus-session-edit.component.ts`** — + `formatSelectedDate()` (weekday `long` + month `long`) → UI language. +3. **`.../simple-counter/habit-tracker/habit-tracker.component.ts`** — `dateRangeLabel` + (month `short`) → UI language. (The habit-tracker weekday _column_ was already guarded + by #8991; only this range label leaked.) +4. **`src/app/ui/pipes/scheduled-date-group.pipe.ts`** — work-view group-header `[title]` + tooltip (weekday `short` + numeric). Low-visibility; applies UI language to the whole + compact formatter (documented tradeoff: the numeric part also follows UI language here + rather than splitting and losing the locale-native separator). +5. **`src/app/features/worklog/worklog.service.ts`** — passes UI language into + `mapArchiveToWorklog` → `formatDayStr` (weekday `short` worklog day headers). Verified + that `locale` param feeds _only_ the spelled-out weekday there. + +Spec mocks updated so the new `isoTextLocale()` calls don't throw +(`TypeError: isoTextLocale is not a function`): `scheduled-date-group.pipe.spec.ts` (+ a new +ISO regression test) and `worklog.service.spec.ts`. The habit-tracker spec already mocked +`isoTextLocale`. + +**NOT leaks (verified guarded / different mechanism — do not touch):** habit-tracker weekday +column, planner-day `dayLabel`, schedule-week/month (all `isoTextLocale`-guarded); the +`formatMonthDay`-based pipes (`short-planned-at`, `local-date-str`, `short-date2`) and the +focus-session chart label are numeric; `planner-calendar-nav` uses the **browser** locale +(`toLocaleDateString(undefined, …)`) — a separate, pre-existing, non-`sv` concern. + +## How the audit was done (repeat it if you suspect more leaks) + +A single-line `grep toLocaleDateString … currentLocale` is **insufficient** — leaks hide +behind the `localeDate` pipe, multi-line `Intl.DateTimeFormat(locale, …)` where `locale` +is assigned earlier, and util indirection (`formatDayStr`, `getWeekdaysMin`). Search +instead for the **spelled-out format tokens** and then trace the locale source: + +```bash +# spelled-out Intl options in .ts +grep -rEn "weekday: *'(long|short|narrow)'|month: *'(long|short|narrow)'" src/app --include='*.ts' | grep -v spec +# spelled-out localeDate pipe usages in templates (E / MMM / EEEE / MMMM) +grep -rEn "localeDate: *'[^']*(E|MMM)" src/app --include='*.html' +``` + +For each hit, confirm whether the locale is `isoTextLocale()`-guarded (safe), the browser +default `undefined` (separate concern), or raw `currentLocale()`/`locale()` (a leak). + +## Verification status + +- **#9055 (branch `feat/https-…-726a40`):** browser-verified against the Cloudflare PR + preview — recur dialog start-date value shows French `mer. 15 juil. 2026` / German + `Mi., 15. Juli 2026` (not Swedish), and the quick-setting dropdown shows German + `Jede Woche am Mittwoch`. Unit tests green; awaiting CI + merge. +- **This branch:** unit-tested (`scheduled-date-group.pipe` 15 incl. new ISO test; + `habit-tracker` 4; `worklog.service` 4). **Not yet browser-verified.** No dedicated spec + for `scheduled-list-page` (routed page, heavy TestBed); rely on browser verification. + +## What's left (do these) + +1. **Push this branch and open the PR** (base `master`): + ```bash + # push is blocked in the sandbox; the user runs it via the `! ` prefix: + ! git push -u origin fix/iso-spelled-out-date-leaks + gh pr create --repo super-productivity/super-productivity --base master \ + --head fix/iso-spelled-out-date-leaks \ + --title "fix(locale): localize remaining ISO 8601 spelled-out date names (#8987)" \ + --body-file + ``` +2. **Browser-verify this branch** on its fresh Cloudflare preview (URL is posted as a PR + comment ~1-2 min after push): Settings → Datetime format = **ISO 8601**, Language = + **Deutsch**; then check (a) the **"Wiederkehrend"** page (`/#/scheduled-list`) date labels + read German `Mi., 15. Juli` not `ons, 15 juli`; (b) habit-tracker range label; (c) a + focus-session-edit dialog date; (d) worklog day headers. +3. **Merge order does not matter** between #9055 and this branch (no shared files). If you + later want the inline expressions unified to `textLocale()`, do it _after_ #9055 merges. +4. **Re-verify #9055's round-2 surfaces** (`getTaskRepeatInfoText` repeat-info text + + add-task-bar date/deadline chips) on #9055's latest preview — the first verification + only covered its commit 1. These are unit-tested and mechanism-proven, so this is + confirmation, not a blocker. + +## Incidental finding (separate, pre-existing, NOT part of either PR) + +`src/assets/i18n/fr.json` uses **single-brace** placeholders (`{weekdayStr}`, +`{dateDayStr}`, `{dayAndMonthStr}`) where ngx-translate needs **double** `{{ }}` — so the +French recurring quick-setting labels render the literal placeholder +("Chaque semaine le {weekdayStr}"). `en.json` and `de.json` are correct. Project rule: +**only edit `en.json`** (other locales come from the translation pipeline), so this should +be reported upstream, not fixed in code here. + +## Commands + +```bash +npm run checkFile # prettier + lint one file (run on every changed .ts) +npm run test:file # single Karma spec (~real Chrome) +git push # blocked in sandbox — user runs `! git push` +``` diff --git a/src/app/features/metric/dialog-focus-session-edit/dialog-focus-session-edit.component.ts b/src/app/features/metric/dialog-focus-session-edit/dialog-focus-session-edit.component.ts index d13c7c3489..fbe7d87d54 100644 --- a/src/app/features/metric/dialog-focus-session-edit/dialog-focus-session-edit.component.ts +++ b/src/app/features/metric/dialog-focus-session-edit/dialog-focus-session-edit.component.ts @@ -173,14 +173,16 @@ export class DialogFocusSessionEditComponent { const date = this.selectedDateStr(); if (date === this.todayStr) return 'Today'; - return new Date(date).toLocaleDateString( - this._dateTimeFormatService.currentLocale(), - { - weekday: 'long', - month: 'long', - day: 'numeric', - }, - ); + // Spelled-out weekday/month names follow the UI language under the ISO 8601 + // option (the `sv` sentinel would otherwise leak Swedish). #8987 follow-up. + const textLocale = + this._dateTimeFormatService.isoTextLocale() ?? + this._dateTimeFormatService.currentLocale(); + return new Date(date).toLocaleDateString(textLocale, { + weekday: 'long', + month: 'long', + day: 'numeric', + }); } private _getChartDates(): string[] { diff --git a/src/app/features/simple-counter/habit-tracker/habit-tracker.component.ts b/src/app/features/simple-counter/habit-tracker/habit-tracker.component.ts index 178585ccc0..b58c996227 100644 --- a/src/app/features/simple-counter/habit-tracker/habit-tracker.component.ts +++ b/src/app/features/simple-counter/habit-tracker/habit-tracker.component.ts @@ -120,7 +120,11 @@ export class HabitTrackerComponent { const first = days[0].date; const last = days[days.length - 1].date; - const locale = this._dateTimeFormatService.currentLocale(); + // Spelled-out `month: 'short'` name follows the UI language under the ISO + // 8601 option (the `sv` sentinel would otherwise leak Swedish). #8987 f/u. + const locale = + this._dateTimeFormatService.isoTextLocale() ?? + this._dateTimeFormatService.currentLocale(); const formatOptions: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric' }; const firstStr = first.toLocaleDateString(locale, formatOptions); const lastStr = last.toLocaleDateString(locale, formatOptions); diff --git a/src/app/features/worklog/worklog.service.spec.ts b/src/app/features/worklog/worklog.service.spec.ts index 705b0f7697..396d63d868 100644 --- a/src/app/features/worklog/worklog.service.spec.ts +++ b/src/app/features/worklog/worklog.service.spec.ts @@ -86,7 +86,7 @@ describe('WorklogService context-aware loading', () => { }, { provide: DateTimeFormatService, - useValue: { currentLocale: () => 'en-US' }, + useValue: { currentLocale: () => 'en-US', isoTextLocale: () => null }, }, ], }); diff --git a/src/app/features/worklog/worklog.service.ts b/src/app/features/worklog/worklog.service.ts index 336bd3ac61..f05d69ffe6 100644 --- a/src/app/features/worklog/worklog.service.ts +++ b/src/app/features/worklog/worklog.service.ts @@ -223,7 +223,11 @@ export class WorklogService { nonArchiveTaskIds, workStartEndForWorkContext, this._dateAdapter.getFirstDayOfWeek(), - this._dateTimeFormatService.currentLocale(), + // Only feeds formatDayStr's spelled-out weekday, which must follow the UI + // language under the ISO 8601 option (the `sv` sentinel would otherwise + // leak Swedish weekday names in worklog day headers). #8987 follow-up. + this._dateTimeFormatService.isoTextLocale() ?? + this._dateTimeFormatService.currentLocale(), ); return { worklog, diff --git a/src/app/pages/scheduled-list-page/scheduled-list-page.component.ts b/src/app/pages/scheduled-list-page/scheduled-list-page.component.ts index afb2a8dc06..fa9c0b943c 100644 --- a/src/app/pages/scheduled-list-page/scheduled-list-page.component.ts +++ b/src/app/pages/scheduled-list-page/scheduled-list-page.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core'; import { T } from '../../t.const'; import { MatDialog } from '@angular/material/dialog'; import { TaskCopy } from '../../features/tasks/task.model'; @@ -66,8 +66,16 @@ export class ScheduledListPageComponent { private _taskRepeatCfgService = inject(TaskRepeatCfgService); private _dateTimeFormatService = inject(DateTimeFormatService); // Exposed so the template can pass the reactive locale to the now-pure - // `localeDate` pipe, preserving re-render on a locale change. - readonly locale = this._dateTimeFormatService.currentLocale; + // `localeDate` pipe, preserving re-render on a locale change. Every localeDate + // usage on this page renders spelled-out weekday/month names (e.g. 'EE, d MMM'), + // so under the ISO 8601 option we follow the UI language (isoTextLocale) rather + // than the `sv` sentinel — which would otherwise leak Swedish ("ons, 15 juli"). + // #8987 follow-up. + readonly locale = computed( + () => + this._dateTimeFormatService.isoTextLocale() ?? + this._dateTimeFormatService.currentLocale(), + ); T: typeof T = T; TODAY_TAG: Tag = TODAY_TAG; taskRepeatCfgs$ = this._store.select(selectTaskRepeatCfgsSortedByTitleAndProject); diff --git a/src/app/ui/pipes/scheduled-date-group.pipe.spec.ts b/src/app/ui/pipes/scheduled-date-group.pipe.spec.ts index 3c277b384f..6e2640d593 100644 --- a/src/app/ui/pipes/scheduled-date-group.pipe.spec.ts +++ b/src/app/ui/pipes/scheduled-date-group.pipe.spec.ts @@ -12,6 +12,8 @@ describe('ScheduledDateGroupPipe', () => { beforeEach(() => { mockDateTimeFormatService = jasmine.createSpyObj('DateTimeFormatService', [], { currentLocale: () => 'en-US', + // null = non-ISO option: the pipe falls back to currentLocale. + isoTextLocale: () => null, }); mockTranslateService = jasmine.createSpyObj('TranslateService', ['instant']); mockTranslateService.instant.and.callFake((key: string) => { @@ -110,6 +112,22 @@ describe('ScheduledDateGroupPipe', () => { expect(result).toMatch(/Mi/i); }); + it('should follow the UI language (isoTextLocale) for the weekday under the ISO option (#8987)', () => { + // ISO 8601 option: currentLocale is the sv sentinel, but isoTextLocale + // carries the UI language ('de-DE'); the weekday must not leak Swedish. + Object.defineProperty(mockDateTimeFormatService, 'currentLocale', { + get: () => () => 'sv', + }); + Object.defineProperty(mockDateTimeFormatService, 'isoTextLocale', { + get: () => () => 'de-DE', + }); + + // Wednesday 2025-01-15 → German "Mi", not Swedish "ons". + const result = pipe.transform('2025-01-15'); + expect(result).toMatch(/Mi/i); + expect(result).not.toMatch(/ons/i); + }); + it('should pass through non-date strings that are not "No date"', () => { // "No tag", "No project" etc. should pass through unchanged const result = pipe.transform('No tag'); diff --git a/src/app/ui/pipes/scheduled-date-group.pipe.ts b/src/app/ui/pipes/scheduled-date-group.pipe.ts index a160dc6dcf..1bc3f65d71 100644 --- a/src/app/ui/pipes/scheduled-date-group.pipe.ts +++ b/src/app/ui/pipes/scheduled-date-group.pipe.ts @@ -42,7 +42,14 @@ export class ScheduledDateGroupPipe implements PipeTransform { } const date = dateStrToUtcDate(value); - const locale = this._dateTimeFormatService.currentLocale(); + // The spelled-out weekday must follow the UI language under the ISO 8601 + // option (the `sv` sentinel would otherwise leak Swedish, e.g. "ons 15/1"). + // This is a compact group-header label, so the whole (short) format follows + // the UI language when ISO is active rather than splitting weekday vs numeric + // and losing the locale-native separator. #8987 follow-up. + const locale = + this._dateTimeFormatService.isoTextLocale() ?? + this._dateTimeFormatService.currentLocale(); // Format with weekday and date: "Wed 1/15" const formatter = new Intl.DateTimeFormat(locale, {