mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
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:
parent
6f88775ea2
commit
7ef7e69e96
8 changed files with 204 additions and 15 deletions
146
docs/handover.md
Normal file
146
docs/handover.md
Normal 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`
|
||||
```
|
||||
|
|
@ -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[] {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ describe('WorklogService context-aware loading', () => {
|
|||
},
|
||||
{
|
||||
provide: DateTimeFormatService,
|
||||
useValue: { currentLocale: () => 'en-US' },
|
||||
useValue: { currentLocale: () => 'en-US', isoTextLocale: () => null },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue