Skip to content

fix(i18n): format the stats date with the selected UI language - #12105

Closed
smwbev wants to merge 3 commits into
stablyai:mainfrom
smwbev:fix/format-stats-date-with-ui-locale
Closed

smwbev wants to merge 3 commits into
stablyai:mainfrom
smwbev:fix/format-stats-date-with-ui-locale

Conversation

@smwbev

@smwbev smwbev commented Aug 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

formatTrackingSince() in StatsPane formats its date with toLocaleDateString(undefined, …), which follows the OS locale rather than the language selected in Settings. On a machine set to en-US, the "Tracking since 11 Jun 2026" header stays English while the rest of the pane is translated.

This was reviewed and confirmed in #11826 (thread) but did not survive the merge, so it is re-sent on its own.

Why it needs a helper

Passing i18n.language straight to Intl is not safe. Plugin language packs register under the synthetic plugin<hex> resource language produced by pluginLanguageResourceId(), and Intl throws a RangeError on it:

Intl.DateTimeFormat.supportedLocalesOf('plugin00720075')  // RangeError

getIntlLocale() resolves the active language to a tag Intl accepts:

  1. if the active language is a plugin pack, use the locale the pack declares (ru-RU);
  2. keep it only if Intl has data for it;
  3. otherwise fall back to DEFAULT_LOCALE — never to the runtime locale, which is the bug being fixed.

The helper is exported so other toLocaleString() call sites can adopt it later; this PR deliberately changes only the one call site the review flagged.

Screenshots

No visual change for an English UI — the date renders identically. The difference appears only when the selected language and the OS locale differ.

Testing

  • pnpm lint
  • pnpm typecheck
  • pnpm test
  • pnpm build
  • Added src/renderer/src/i18n/intl-locale.test.ts — five cases driving getIntlLocale() through the public API (registering a pack, switching languages) rather than mocking internals: built-in locale passthrough, plugin resource language resolving to the declared ru-RU, unregistered plugin tag and unsupported-but-valid tag both falling back to DEFAULT_LOCALE, and the synthetic tag still throwing when handed to Intl directly.

pnpm test: 43 491 passed, 79 skipped. Two files fail on this machine — src/main/ssh/ssh-posix-command-wrapper.test.ts (3) and src/relay/agent-exec-handler.test.ts (2). Both fail identically on a clean main with this branch stashed, so they are pre-existing environment failures.

Note on CI: this PR comes from a fork, so PR Checks sits at action_required until a maintainer approves the workflow run — nothing has failed. The checks above were run locally.

AI Review Report

Reviewed with Claude Opus 5.

  • Cross-platform. No platform branching. Intl data availability differs between Electron builds and Node versions, which is exactly why the result is validated through supportedLocalesOf() instead of being trusted; the fallback keeps behaviour deterministic on every platform.
  • SSH / remote / local. Renderer-only formatting change, no process, path, or network surface.
  • Agents / integrations / git providers. Not touched.
  • Performance. One supportedLocalesOf() call per render of a single header; negligible, and the previous code already constructed a formatter on the same path.
  • UI quality. English output is byte-identical. Localized builds now follow the selected language.
  • Failure modes checked: synthetic plugin tag (throws — caught), well-formed but unsupported tag (empty array — falls back), plugin pack with a declared locale (resolves), no plugin pack (passes the active language through).

Security Audit

Reviewed with Claude Opus 5. No new input handling, command execution, path handling, auth, secrets, dependency, or IPC surface. The only external input is the locale string a plugin declares in its manifest, which is already validated by parsePluginLanguagePackArtifact and is passed to Intl inside a try/catch that cannot escape. No follow-up needed.

Notes

Found while maintaining a full Russian language pack for Orca — smwbev/orca-russian, 11 676 strings, 98.2 % of en.json. The plugin-pack branch of the helper is not hypothetical: that is the path every plugin-supplied locale takes.

X: @silentmegawatt · GitHub: @smwbev

`formatTrackingSince()` builds its date with `toLocaleDateString(undefined, …)`,
which follows the OS locale rather than the language chosen in Settings. On a
machine set to en-US the "Tracking since …" header stays English even when the
rest of the pane is translated.

Added `getIntlLocale()` and used it for that call. The helper handles the case
that makes this non-trivial: plugin language packs register under the synthetic
`plugin<hex>` resource language from `pluginLanguageResourceId()`, which `Intl`
rejects outright, so it resolves to the BCP-47 tag the pack declares and falls
back to the default locale when `Intl` has no data for the tag.

This was reviewed and confirmed in stablyai#11826 but did not survive the merge, so it
is re-sent on its own.
@greptile-apps

greptile-apps Bot commented Aug 2, 2026 •

Copy link
Copy Markdown

Greptile Summary

This PR fixes a localization bug where formatTrackingSince() in StatsPane formatted dates using the OS locale instead of the UI language selected in Settings. It introduces getIntlLocale() — a small helper that maps the active i18n.language to a valid BCP-47 tag for Intl, safely handling synthetic plugin<hex> resource language identifiers that Intl rejects.

  • New getIntlLocale() helper (i18n.ts): resolves plugin resource languages to their declared locale, validates via Intl.DateTimeFormat.supportedLocalesOf(), and falls back to DEFAULT_LOCALE ('en') rather than silently deferring to the OS locale.
  • StatsPane.tsx: one-line change replacing toLocaleDateString(undefined, …) with toLocaleDateString(getIntlLocale(), …).
  • intl-locale.test.ts: five test cases driving getIntlLocale() end-to-end via the public API, covering built-in locale passthrough, plugin-pack locale resolution, unsupported tags, and the throwing case.

Confidence Score: 5/5

Safe to merge — the change is a targeted, renderer-only fix to a single date-formatting call site, with robust fallback logic and no new external surfaces.

The helper is small, self-contained, and its three execution paths (built-in locale, plugin-pack locale, fallback) are each covered by a test. The try/catch around supportedLocalesOf ensures invalid tags never propagate. The only observable difference from main is that the tracking-since date now follows the UI language rather than the OS locale — which is the intended fix.

Files Needing Attention: No files require special attention.

Important Files Changed

Filename Overview
src/renderer/src/i18n/i18n.ts Adds getIntlLocale() — correctly resolves plugin resource languages, validates with supportedLocalesOf, and falls back to DEFAULT_LOCALE; logic is sound and well-guarded.
src/renderer/src/components/stats/StatsPane.tsx Single-line fix replacing undefined with getIntlLocale() in toLocaleDateString; change is minimal and correct.
src/renderer/src/i18n/intl-locale.test.ts New test file with five well-structured cases exercising getIntlLocale() through the public API; stubs supportedLocalesOf to decouple from ICU data availability.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["formatTrackingSince(timestamp)"] --> B["getIntlLocale()"]
    B --> C["active = i18n.language"]
    C --> D{"Is it a plugin\nresource language?"}
    D -- "Yes: find in pluginLanguagePacks" --> E["candidate = pack.locale\n(e.g. 'ru-RU')"]
    D -- "No" --> F["candidate = active\n(e.g. 'es')"]
    E --> G["Intl.DateTimeFormat\n.supportedLocalesOf(candidate)"]
    F --> G
    G -- "returns non-empty array" --> H["return result[0]"]
    G -- "returns empty array" --> I["return DEFAULT_LOCALE ('en')"]
    G -- "throws RangeError" --> I
    H --> J["date.toLocaleDateString(locale, opts)"]
    I --> J
Loading

Reviews (3): Last reviewed commit: "test(i18n): stub ICU lookup so the local..." | Re-trigger Greptile

Comment thread src/renderer/src/i18n/intl-locale.test.ts Outdated
@coderabbitai

coderabbitai Bot commented Aug 2, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e2aae63e-0e4f-4147-b716-836923bc4b51

📥 Commits

Reviewing files that changed from the base of the PR and between e1efc59 and 2b57d26.

📒 Files selected for processing (1)
  • src/renderer/src/i18n/intl-locale.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/renderer/src/i18n/intl-locale.test.ts

📝 Walkthrough

Walkthrough

The i18n module adds getIntlLocale, which resolves and validates the active locale and falls back to DEFAULT_LOCALE when required. Tests cover built-in locales, plugin locales, unsupported tags, and invalid tags. StatsPane uses the resolved locale when formatting tracking dates.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main i18n fix for stats date formatting.
Description check ✅ Passed The description includes all required sections, testing results, review coverage, security audit, and relevant notes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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.

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
src/renderer/src/i18n/intl-locale.test.ts (1)

10-22: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test getIntlLocale() directly.

These tests only verify Intl.DateTimeFormat.supportedLocalesOf(). They do not exercise getIntlLocale(), plugin-pack lookup, or the fallback branches. Add cases that configure a synthetic plugin resource language with a declared locale, an unsupported declared locale, and an invalid active locale. Assert that the helper returns the declared locale or DEFAULT_LOCALE as appropriate.

Based on the supplied test body and getIntlLocale() implementation.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bf024afd-32b0-407a-9345-4107fb812ae5

📥 Commits

Reviewing files that changed from the base of the PR and between 0ae9174 and 88e14d2.

📒 Files selected for processing (3)
  • src/renderer/src/components/stats/StatsPane.tsx
  • src/renderer/src/i18n/i18n.ts
  • src/renderer/src/i18n/intl-locale.test.ts

…nditions

The suite pinned the Intl behaviour that motivates the helper but never called
it, so a regression in the pack lookup or the supportedLocalesOf guard would
have gone unnoticed. Drives the function through the public API — registering a
pack and switching languages — rather than mocking module internals, so the
plugin path is covered end to end.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 504ea730-7d6d-4a18-8880-ddcd2019e777

📥 Commits

Reviewing files that changed from the base of the PR and between 88e14d2 and e1efc59.

📒 Files selected for processing (1)
  • src/renderer/src/i18n/intl-locale.test.ts

Comment thread src/renderer/src/i18n/intl-locale.test.ts
supportedLocalesOf() answers from whatever ICU data the runtime carries, so
asserting on real tags made the branch coverage depend on the build: a small-icu
Node would fail the passthrough case for reasons unrelated to the helper.

The branch assertions now stub the lookup and describe the logic instead. The
RangeError case keeps its unstubbed assertion, since rejecting the synthetic tag
is a property of the tag itself rather than of ICU data.
@smwbev

smwbev commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

@brennanb2025 — flagging this one for you since CODEOWNERS did not pick it up.

It touches src/renderer/src/i18n/i18n.ts but not i18n/locales/, so no reviewer was auto-assigned, while its sibling #12208 routed to you automatically. Both come out of the same pass that produced #11826 (merged):

No rush on any of these — mostly wanted to make sure #12105 is not invisible for want of a CODEOWNERS entry.

nwparker pushed a commit that referenced this pull request Aug 4, 2026
formatTrackingSince() used the OS locale rather than the language chosen in
Settings. getIntlLocale() resolves the active i18n language to a BCP-47 tag,
mapping the synthetic plugin<hex> resource language that Intl rejects back to
the pack's real locale.

Co-authored-by: Evgenii <kumiro@me.com>
nwparker pushed a commit that referenced this pull request Aug 4, 2026
Eight renderer call sites built Intl.RelativeTimeFormat(undefined, ...), which
resolves to the OS locale, so relative timestamps rendered in Korean on a
Korean-locale machine even with the UI language set to English. Unlike
DateTimeFormat, RelativeTimeFormat emits language words, so it must follow the
UI language.

Rebased onto main: the formatter now resolves through getIntlLocale() (#12105)
rather than i18n.resolvedLanguage, so the synthetic plugin<hex> resource
language cannot reach the constructor and throw. GitHubItemDialog and
PullRequestPage no longer own their formatter — main moved it into
work-item-state-presentation, which is converted instead, along with the newly
added site there.

Co-authored-by: moseoh <azqazq195@gmail.com>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>

Co-authored-by: Orca <help@stably.ai>
@nwparker

nwparker commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Merged into main via #12514, which consolidated the open community translation work into one branch. Your commit is on main with you as its author — I used a rebase merge specifically so every contributor's commit and co-author trailer survived intact rather than being squashed into one.

Closing this in favour of that. Thank you for the fix, and sorry it took as long as it did to get through.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants