Skip to content

fix(desktop): timestamps follow the OS locale instead of en-US - #6190

Merged
juliusmarminge merged 2 commits into
pingdotgg:mainfrom
brzzdev:fix/desktop-system-locale-timestamps
Aug 15, 2026
Merged

fix(desktop): timestamps follow the OS locale instead of en-US#6190
juliusmarminge merged 2 commits into
pingdotgg:mainfrom
brzzdev:fix/desktop-system-locale-timestamps

Conversation

@brzzdev

@brzzdev brzzdev commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Fixes #6178.

Problem

On the packaged desktop app, the default Timestamp format: Locale setting always renders US 12-hour times — 3:44 PM on a machine whose region asks for 15:44.

The app ships only the en-US Chromium locale pak (DESKTOP_ELECTRON_LANGUAGES, scripts/build-desktop-artifact.ts:629), and Chromium resolves its default Intl locale from the application locale rather than from the OS. Passing undefined to Intl.DateTimeFormat in the renderer therefore means en-US however the machine is configured, so the "locale" branch of getTimestampFormatOptions — which deliberately omits hour12 and defers to the runtime — silently means "US 12-hour" on desktop. DEFAULT_TIMESTAMP_FORMAT is "locale", so this is the out-of-the-box experience for every desktop user outside the US.

Measured inside the packaged renderer (0.0.33, macOS set to en_GB):

{
  intlDefaultLocale: "en-US",          // ← should be en-GB
  navigatorLanguages: ["en-US", "en-GB"],
  renderedLikeT3:  "4:04 PM",          // Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit" })
  withExplicitEnGB: "16:04",
  timeZone: "Europe/London"            // ← correct
}

The time zone resolving correctly while the locale does not is the tell: the OS is reachable from the process, and it is specifically the application locale that the pak trim pins.

Fix

Read the OS locale in the main process with app.getSystemLocale() — which the pak trim does not affect, unlike app.getLocale() — expose it over the existing desktop bridge, and format timestamps with it. The bridge member follows getAppBranding: a sync sendSync getter, so the cached, synchronous formatter needs no async plumbing.

Web is unchanged: with no bridge present the resolver returns undefined and the browser keeps resolving its own locale, which is already correct there.

resolveTimestampLocale normalizes POSIX-style tags (en_GBen-GB) and falls back to the runtime default rather than throwing if the host ever reports something ICU rejects, since the formatter backs every timestamp in the UI.

Before / after

Chat timestamps, rendered by the real client under each locale. Same fixture, same crop; only the locale the formatter receives differs.

Before (en-US — what desktop shows today) After (OS locale en-GB)

A note on how these were produced, so the evidence is not overstated: the bug does not reproduce in a dev run, because electronLanguages is an electron-builder packaging option and pnpm start has every locale pak. These are the web client driven under a locale-pinned Chromium context — en-US for before, the OS locale en-GB for after — which is exactly the pair of locales the formatter receives on either side of this change. The IPC half is covered by types and by the getAppBranding precedent it mirrors, not by these images.

Surfaces

  • Desktop — the only surface affected, and the only one changed.
  • Web — unaffected; browsers resolve their own locale correctly, and the resolver returns undefined without a bridge.
  • Mobile — unaffected; React Native has no Chromium pak trim.
  • ContractsDesktopBridge gains one optional member.
  • Docs — no user docs describe timestamp formatting, so none changed.

Out of scope

Three other call sites format with an implicit locale and are wrong on desktop for the same reason. Left alone to keep this to one concern; happy to follow up:

  • apps/web/src/components/settings/ConnectionsSettings.tsx:144
  • apps/web/src/components/clerk/MobileClientsUserProfilePage.logic.ts:3
  • packages/client-runtime/src/state/threadSettled.ts:300 (snooze preset labels)

Testing

resolveTimestampLocale is unit tested for the tag it picks and for the hour cycle that results, including the en-GB15:44 / en-US3:44 PM pair. ElectronApp.test.ts covers the OS-locale passthrough and the POSIX normalization. Targeted vp test run on both files (25 and 8 passing), plus typecheck and lint on the three touched packages.

Four other ElectronApp test stubs gained the new systemLocale member. Those four are among 22 desktop test files that fail to import in my environment on main — an unrelated vi.mock/electron issue, identical before and after this change — so they are type-correct but I could not execute them locally. ElectronApp.test.ts itself is not affected and runs green.


Written by Claude Opus 5 in T3 Code.


Note

Low Risk
Localized display-only timestamp formatting on desktop; web unchanged. Fallbacks avoid throws on bad locale tags.

Overview
Fixes packaged desktop timestamps always rendering as US 12-hour when Timestamp format: Locale is selected, because Chromium’s default Intl locale is pinned to en-US (only that locale pak ships).

The main process now reads Electron.app.getSystemLocale(), normalizes POSIX tags (en_GBen-GB), and exposes the result via a new sync getSystemLocale IPC path on desktopBridge (mirroring getAppBranding).

In timestampFormat.ts, resolveTimestampLocale validates the host tag and feeds it into cached Intl.DateTimeFormat instances so wall-clock times follow the OS (e.g. 15:44 for en-GB). Without a bridge (web), behavior is unchanged (undefined → browser default). Invalid tags fall back instead of breaking the UI.

DesktopBridge gains optional getSystemLocale. Desktop test stubs pick up systemLocale on ElectronApp; new unit tests cover locale resolution and Electron passthrough/normalization.

Reviewed by Cursor Bugbot for commit 244ca16. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Fix timestamp formatting to use OS locale instead of en-US

  • Adds systemLocale to the ElectronApp service, reading Electron.app.getSystemLocale() and normalizing POSIX-style tags (e.g. en_GBen-GB).
  • Exposes the locale to the renderer via a new synchronous IPC method (getSystemLocale) and window.desktopBridge.getSystemLocale().
  • Adds resolveTimestampLocale in timestampFormat.ts to validate the host-provided locale using Intl.DateTimeFormat.supportedLocalesOf, falling back to the runtime default on invalid or missing values.
  • All Intl.DateTimeFormat timestamp formatters now use the resolved OS locale instead of an implicit en-US default.
  • Behavioral Change: timestamps in the desktop app will now render according to the OS locale (e.g. 24-hour clock for en-GB), which may differ visually from previous output.

Macroscope summarized 244ca16.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ebac7ab7-a9e2-4012-8f24-653766367fba

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Aug 11, 2026
macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Aug 11, 2026
@macroscopeapp

macroscopeapp Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved 0fd360a

This is a focused bug fix that makes desktop timestamps respect the OS locale instead of being hardcoded to en-US. The implementation is self-contained, well-tested, and properly handles edge cases like POSIX locale format normalization.

You can customize Macroscope's approvability policy. Learn more.

@brzzdev
brzzdev force-pushed the fix/desktop-system-locale-timestamps branch from f2dc81b to 0fd360a Compare August 11, 2026 16:09
@macroscopeapp
macroscopeapp Bot dismissed their stale review August 11, 2026 16:09

Dismissing prior approval to re-evaluate 0fd360a

@brzzdev

brzzdev commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Pushed a follow-up commit for the review. Rebased onto latest main.

Fixed — mixed-language tooltips. Correct, and it was a regression this PR introduced. formatChatTimestampTooltip builds ${time}, ${day}${ordinalSuffix} ${month} ${year}, where the ordinal suffix and the day-before-month order are hardcoded English, so localizing the month alone really does yield 15:44, 4th Juni 2026. monthNameFormatter is back on the runtime locale with a comment recording why, which also drops a line this fix never needed. Localizing that whole label is a separate change.

Fixed — adapter boundary. POSIX normalization (en_GBen-GB) moved to ElectronApp.systemLocale, where the macOS NSLocale behavior that causes it belongs, and the packaging exposition left the renderer. The renderer still falls back rather than throwing on a tag it cannot use — that guard is not platform knowledge, it is that this formatter backs every timestamp in the UI and a RangeError there would blank all of them.

Fixed — desktop coverage. Two tests added to ElectronApp.test.ts, for the passthrough and for the normalization. Worth correcting one point: that file is not among the desktop tests that fail to import, and it runs green locally (6 → 8). The import failures are a separate pre-existing vi.mock/electron issue affecting 22 files identically on main; four of them happen to hold ElectronApp stubs that needed the new member.

Not doing — user documentation. docs/user/ has no page covering timestamps, appearance, or the Settings surface this lives on, so there is nothing to update; the rule points at behavior changes a user would notice, and this restores the behavior the existing setting already promises. Writing a new user-docs page to carry a bug fix is a second concern, and I would rather you tell me you want one than have it arrive unasked.

Not doing — shared ElectronApp test builder. Agreed on the smell, but collapsing four unrelated test files into a shared stub is exactly the "also" that gets a PR split. Happy to do it separately if you want it.

Verification after the change: ElectronApp.test.ts 8 passing, timestampFormat.test.ts 25 passing, typecheck and lint clean on the three touched packages. The before/after images are unaffected — they show formatShortTimestamp in the chat timeline, not the tooltip.

@juliusmarminge
juliusmarminge enabled auto-merge (squash) August 15, 2026 10:43
The packaged desktop app ships only the en-US Chromium locale pak, and
Chromium resolves its default Intl locale from the application locale
rather than from the OS. Passing `undefined` to Intl.DateTimeFormat in
the renderer therefore always meant "en-US", so the default "Locale"
timestamp setting rendered 3:44 PM on a machine whose region asks for
15:44.

Expose the OS locale over the desktop bridge via app.getSystemLocale(),
which the pak trim does not affect, and format timestamps with it.
Web keeps passing `undefined` and resolves from the browser as before.
Review follow-up.

The tooltip month name is back on the runtime locale. Localizing it from
the OS locale while the surrounding ordinal suffix and day-before-month
order stay English produced mixed-language labels such as
"15:44, 4th Juni 2026". Localizing that whole label is a separate change.

POSIX identifier normalization (en_GB -> en-GB) moves to the Electron
service, where the macOS NSLocale behavior that causes it belongs, so the
renderer no longer carries platform knowledge. The renderer keeps a
fallback for a tag it cannot use, since this formatter backs every
timestamp in the UI.

Adds focused ElectronApp coverage for both the passthrough and the
normalization.
@juliusmarminge
juliusmarminge force-pushed the fix/desktop-system-locale-timestamps branch from 0fd360a to 244ca16 Compare August 15, 2026 12:08

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One consistency issue found in apps/web/src/timestampFormat.ts: the day-aware timestamp now mixes the host locale (time) with the runtime-default locale (numeric date). Details inline.

Posted via Macroscope — UI Consistency

Comment on lines +87 to +89
// Deliberately not the host locale: the tooltip's ordinal suffix and
// day-before-month order below are English, so a localized month alone would
// read "4th Juni 2026". Localizing the whole label is a separate change.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The English-tooltip exception is well justified, but the two numeric date formatters below (numericDateFormatter, numericDateWithYearFormatter) are still built with undefined while the time formatter now uses timestampLocale. formatDayAwareTimestamp concatenates the two, so on packaged desktop — where the runtime default is pinned to en-US — an en-GB machine renders 8/13 15:44 instead of 13/08 15:44, i.e. UK hour cycle with US month/day order in the same label. That also contradicts the formatDayAwareTimestamp docstring, which promises "locale digit order".

These formatters emit no English words, so the reason given here for monthNameFormatter doesn't apply to them; suggest passing timestampLocale to both (they are declared after the timestampLocale binding, so no reordering is needed), or adding a short note if leaving them on the runtime default is deliberate.

Posted via Macroscope — UI Consistency

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 244ca16. Configure here.


// Deliberately not the host locale: the tooltip's ordinal suffix and
// day-before-month order below are English, so a localized month alone would
// read "4th Juni 2026". Localizing the whole label is a separate change.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chat dates stay US-ordered

Medium Severity

formatDayAwareTimestamp now formats the time with timestampLocale but still builds the date prefix via numericDateFormatter and numericDateWithYearFormatter on the runtime default. On packaged desktop that default stays en-US, so older chat timestamps pair an OS-locale time with US M/D order. Unlike monthNameFormatter, these numeric formatters have no English-only scaffolding that would make the host locale unsafe.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 244ca16. Configure here.

@juliusmarminge
juliusmarminge merged commit b72d5d7 into pingdotgg:main Aug 15, 2026
13 of 15 checks passed
@juliusmarminge

Copy link
Copy Markdown
Member

The current head still has the date-prefix locale bug identified by Macroscope and Bugbot. I prepared and verified the focused fix, but maintainerCanModify is disabled and GitHub rejected the guarded push to the fork.

Please update both numericDateFormatter and numericDateWithYearFormatter in apps/web/src/timestampFormat.ts to construct Intl.DateTimeFormat(timestampLocale, ...), and add coverage proving an en-GB desktop host renders an older chat timestamp as 12/08 15:44 rather than mixing 8/12 with a 24-hour time.

Verified locally against head 244ca169ca860663e7229c1b6e9f176c9c06a9f0: 39 focused tests passed; targeted lint and format passed; web, desktop, and contracts typechecks passed. Auto-merge should remain disabled until the actionable review is fixed; GitHub currently rejects the disable-auto-merge mutation for this PR.

github-actions Bot added a commit to omarcresp/t3code-flake that referenced this pull request Aug 15, 2026
## What's Changed
* fix(desktop): route mouse thumb buttons to the in-app browser by @akosbalogh in pingdotgg/t3code#4459
* fix(web): keep the final segment of directory paths with a trailing separator by @jorvarea in pingdotgg/t3code#5460
* Keep block code plain when copying from rendered markdown by @yashranaway in pingdotgg/t3code#4468
* fix(web): add web app manifest so installed app keeps its scope by @Albro3459 in pingdotgg/t3code#4306
* Skip user hooks during Claude capability probes by @yashranaway in pingdotgg/t3code#4466
* fix(mobile): use Android monospace font family by @tastelessjolt in pingdotgg/t3code#4609
* fix(desktop): timestamps follow the OS locale instead of en-US by @brzzdev in pingdotgg/t3code#6190
* fix(web): keep multi-select questions open after the first click by @RaitP1 in pingdotgg/t3code#6646
* fix(web): stop clipping the changed-files expand hover on Windows by @mrmg in pingdotgg/t3code#6545
* fix(server): allow long-running git pushes by @devchaudhary24k in pingdotgg/t3code#6499
* fix(desktop): keep probing backend readiness while the process is alive by @lgwacker in pingdotgg/t3code#5526
* fix(server): allow install scripts in npm-global provider updates by @hey-jj in pingdotgg/t3code#5646
* fix: detect SSH remotes with non-git user prefixes (e.g. gitlab@) by @JackatDJL in pingdotgg/t3code#3649
* fix(web): describe what Ultracode does in the Reasoning picker by @delltrak in pingdotgg/t3code#6092
* fix(server): settle pending user-input requests when a Claude session stops by @AaronAbuUsama in pingdotgg/t3code#5127
* fix(server): stop replaying a command receipt for a different aggregate by @ostapondo in pingdotgg/t3code#5246
* fix(server): settle snoozed threads immediately by @0bkevin in pingdotgg/t3code#5379
* fix(mobile): prevent crash on sign out in settings by @shubhu121 in pingdotgg/t3code#4899
* fix(mobile): local-checkout threads record their branch so PR badges show by @Zeus-Deus in pingdotgg/t3code#4986
* fix(web): contain long approval commands by @Serendeep in pingdotgg/t3code#6503
* feat(web): make right panel maximize bindable by @husseinraoouf in pingdotgg/t3code#5091
* fix(server): respect inherited OPENCODE_CONFIG_CONTENT by @jonocodes in pingdotgg/t3code#4242
* fix(marketing): detect Mac chip on homepage download button by @mahdibm-dev in pingdotgg/t3code#4197
* Keep the server alive when a response write hits a dead socket by @yashranaway in pingdotgg/t3code#4470
* Limit physical key fallback to non-Latin layout output by @yashranaway in pingdotgg/t3code#4469
* fix: restore CLAUDE.md symlink target by @NgoQuocViet2001 in pingdotgg/t3code#3929
* fix(clients): default clone destination to folder plus repo name by @inayayousfi in pingdotgg/t3code#5989
* fix(web): keep timestamp date and time in the same locale by @juliusmarminge in pingdotgg/t3code#7081
* feat(desktop): add signal macOS DMG installer background by @Brechard in pingdotgg/t3code#6201

## New Contributors
* @akosbalogh made their first contribution in pingdotgg/t3code#4459
* @jorvarea made their first contribution in pingdotgg/t3code#5460
* @yashranaway made their first contribution in pingdotgg/t3code#4468
* @Albro3459 made their first contribution in pingdotgg/t3code#4306
* @tastelessjolt made their first contribution in pingdotgg/t3code#4609
* @brzzdev made their first contribution in pingdotgg/t3code#6190
* @RaitP1 made their first contribution in pingdotgg/t3code#6646
* @devchaudhary24k made their first contribution in pingdotgg/t3code#6499
* @lgwacker made their first contribution in pingdotgg/t3code#5526
* @JackatDJL made their first contribution in pingdotgg/t3code#3649
* @delltrak made their first contribution in pingdotgg/t3code#6092
* @AaronAbuUsama made their first contribution in pingdotgg/t3code#5127
* @0bkevin made their first contribution in pingdotgg/t3code#5379
* @shubhu121 made their first contribution in pingdotgg/t3code#4899
* @Zeus-Deus made their first contribution in pingdotgg/t3code#4986
* @husseinraoouf made their first contribution in pingdotgg/t3code#5091
* @jonocodes made their first contribution in pingdotgg/t3code#4242
* @mahdibm-dev made their first contribution in pingdotgg/t3code#4197
* @NgoQuocViet2001 made their first contribution in pingdotgg/t3code#3929
* @inayayousfi made their first contribution in pingdotgg/t3code#5989

**Full Changelog**: pingdotgg/t3code@v0.0.34-nightly.20260815.1101...v0.0.34-nightly.20260815.1102

Upstream release: https://github.com/pingdotgg/t3code/releases/tag/v0.0.34-nightly.20260815.1102
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M 30-99 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Desktop "Timestamp format: Locale" always renders US 12-hour times because the packaged app ships only the en-US Chromium locale

2 participants