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.
This commit is contained in:
Johannes Millan 2026-07-16 00:03:42 +02:00
parent 6fefd741c5
commit eab30198dc
8 changed files with 204 additions and 15 deletions

146
docs/handover.md Normal file
View file

@ -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 `<mat-calendar>` 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 <a body summarizing the 5 sites>
```
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 <file> # prettier + lint one file (run on every changed .ts)
npm run test:file <spec> # single Karma spec (~real Chrome)
git push # blocked in sandbox — user runs `! git push`
```

View file

@ -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[] {

View file

@ -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);

View file

@ -86,7 +86,7 @@ describe('WorklogService context-aware loading', () => {
},
{
provide: DateTimeFormatService,
useValue: { currentLocale: () => 'en-US' },
useValue: { currentLocale: () => 'en-US', isoTextLocale: () => null },
},
],
});

View file

@ -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,

View file

@ -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);

View file

@ -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');

View file

@ -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, {