Skip to content

fix: incorrect 24h time detection for non-AM/PM locales#29036

Open
Akash504-ai wants to merge 4 commits into
calcom:mainfrom
Akash504-ai:fix/time-format-24h-detection
Open

fix: incorrect 24h time detection for non-AM/PM locales#29036
Akash504-ai wants to merge 4 commits into
calcom:mainfrom
Akash504-ai:fix/time-format-24h-detection

Conversation

@Akash504-ai
Copy link
Copy Markdown
Contributor

@Akash504-ai Akash504-ai commented Apr 26, 2026

What does this PR do?

Fixes incorrect 24h time detection for locales that do not use "AM/PM" with "M" (e.g., Japanese, Chinese, Korean).

Previously, detection relied on string matching:

new Intl.DateTimeFormat(undefined, { hour: "numeric" }).format(0).match(/M/i)

This assumes 12h formats always include "AM/PM", which is not true.

For example (Japanese locale):

new Intl.DateTimeFormat("ja-JP", {
  hour: "numeric",
  minute: "numeric",
  hour12: true
}).format(new Date())
// → "午前10:50"

Even though this is a 12-hour format, it does not contain "AM/PM", so:

new Intl.DateTimeFormat("ja-JP", { hour: "numeric" }).format(0).match(/M/i)
// → null

This leads to incorrect 24h detection.

This PR replaces the logic with:

new Intl.DateTimeFormat(undefined, { hour: "numeric" }).resolvedOptions().hourCycle

which provides a reliable and locale-safe way to detect 12h/24h formats.


Visual Demo

The attached video demonstrates:

  • A valid 12-hour format without "AM/PM" (午前10:50)
  • Failure of previous detection logic (null)
  • Correct detection using hourCycle
Event.types._.Cal.com.-.Brave.2026-04-27.10-50-36.online-video-cutter.com.mp4

Exact problem in your code :

image

Mandatory Tasks

  • I have self-reviewed the code
  • I have updated the developer docs if required (N/A)
  • I confirm automated tests are in place

How should this be tested?

Run:

yarn vitest packages/lib/timeFormat.test.ts

Expected:

  • h23 / h24 → true
  • h11 / h12 → false

@github-actions
Copy link
Copy Markdown
Contributor

Welcome to Cal.diy, @Akash504-ai! Thanks for opening this pull request.

A few things to keep in mind:

  • This is Cal.diy, not Cal.com. Cal.diy is a community-driven, fully open-source fork of Cal.com licensed under MIT. Your changes here will be part of Cal.diy — they will not be deployed to the Cal.com production app.
  • Please review our Contributing Guidelines if you haven't already.
  • Make sure your PR title follows the Conventional Commits format.

A maintainer will review your PR soon. Thanks for contributing!

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 26, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d95524d4-61e3-4730-a76b-8469eb64059b

📥 Commits

Reviewing files that changed from the base of the PR and between b8d9bf9 and 634524d.

📒 Files selected for processing (1)
  • packages/lib/timeFormat.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/lib/timeFormat.ts

📝 Walkthrough

Walkthrough

The isBrowserLocale24h function now determines 24-hour vs 12-hour preference by constructing an Intl.DateTimeFormat with hour: "numeric" and checking resolvedOptions().hourCycle, storing the boolean result in local storage. A new Vitest test suite timeFormat.test.ts was added that mocks @calcom/lib/webstorage, spies on Intl.DateTimeFormat to return hourCycle values, and asserts the function returns true for "h23" and false for "h12".

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: fixing 24h time detection for locales without AM/PM markers by replacing string-based detection with Intl.DateTimeFormat.
Description check ✅ Passed The description is directly related to the changeset, explaining the problem, the solution, testing approach, and confirming mandatory tasks were completed.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
packages/lib/timeFormat.test.ts (1)

12-39: Consider broadening test coverage.

The PR description lists h24/h11 and the cached-localStorage early-return paths as expected behavior, but only h23/h12 are exercised. Worth adding cases for completeness:

  • hourCycle === "h24"true
  • hourCycle === "h11"false
  • hourCycle === undefinedfalse (safe fallback)
  • localStorage.getItem returning "true"/"false" → returns cached value without invoking Intl.DateTimeFormat
  • After computing, localStorage.setItem is called with the derived boolean string

Also consider vi.resetAllMocks() instead of vi.restoreAllMocks() if you want the vi.fn() instances from the vi.mock factory to have their call history cleared between tests — restoreAllMocks only restores spies created with vi.spyOn.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lib/timeFormat.test.ts` around lines 12 - 39, Add tests exercising
the missing hourCycle values and the localStorage cache paths: extend
timeFormat.test.ts to assert isBrowserLocale24h() returns true for hourCycle
"h24" and false for "h11", false when resolvedOptions().hourCycle is undefined,
and verify that when localStorage.getItem returns "true" or "false" the function
returns that cached boolean without calling Intl.DateTimeFormat; also assert
that when the value is computed the code calls localStorage.setItem with the
stringified boolean. Replace vi.restoreAllMocks() with vi.resetAllMocks() in
beforeEach to clear mock call history between tests so vi.mock factory fns are
reset.
packages/lib/timeFormat.ts (1)

1-6: Update the stale file header comment.

The header still describes the old AM/'01 h' string-detection approach, which no longer matches the new hourCycle-based implementation. Worth refreshing so future readers aren't misled.

📝 Proposed update
 /*
- * Detects navigator locale 24h time preference
- * It works by checking whether hour output contains AM ('1 AM' or '01 h')
- * based on the user's preferred language
- * defaults to 'en-US' (12h) if no navigator language is found
+ * Detects navigator locale 24h time preference using
+ * Intl.DateTimeFormat().resolvedOptions().hourCycle so it works
+ * for locales whose AM/PM markers don't contain "M" (e.g. ja, zh, ko).
  */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/lib/timeFormat.ts` around lines 1 - 6, The header comment at the top
of packages/lib/timeFormat.ts is outdated (mentions AM/'01 h' string-detection)
and should be replaced with a short, accurate description that the module
detects the user's 24h/12h preference using
Intl.DateTimeFormat().resolvedOptions().hourCycle (falling back to
navigator.language or 'en-US' when unavailable) and explains the default
behavior; update the top-of-file comment to reference the hourCycle-based
implementation and navigator.language fallback so future readers aren't misled.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/lib/timeFormat.test.ts`:
- Around line 17-39: The test uses three `as any` casts when mocking
Intl.DateTimeFormat; replace them with proper types by making the mock
implementation return a correctly typed object (e.g., implement resolvedOptions
on a Partial<Intl.DateTimeFormat> and cast to Intl.DateTimeFormat, or declare
the mock function signature as (): Intl.DateTimeFormat => ...) so the
spyOn(mock) satisfies Intl.DateTimeFormatConstructor without using `any`; apply
this change to both tests that mock Intl.DateTimeFormat and keep the assertions
against isBrowserLocale24h() unchanged.

In `@packages/lib/timeFormat.ts`:
- Around line 45-48: The code unnecessarily casts resolvedOptions() to any to
read hourCycle; remove the cast and use the proper typed value by declaring
hourCycle via const hourCycle = formatter.resolvedOptions().hourCycle (letting
TypeScript infer the type "h11" | "h12" | "h23" | "h24" | undefined) and keep
the existing is24h check (const is24h = hourCycle === "h23" || hourCycle ===
"h24"); update any surrounding logic that assumed a loose type to use this typed
hourCycle instead.

---

Nitpick comments:
In `@packages/lib/timeFormat.test.ts`:
- Around line 12-39: Add tests exercising the missing hourCycle values and the
localStorage cache paths: extend timeFormat.test.ts to assert
isBrowserLocale24h() returns true for hourCycle "h24" and false for "h11", false
when resolvedOptions().hourCycle is undefined, and verify that when
localStorage.getItem returns "true" or "false" the function returns that cached
boolean without calling Intl.DateTimeFormat; also assert that when the value is
computed the code calls localStorage.setItem with the stringified boolean.
Replace vi.restoreAllMocks() with vi.resetAllMocks() in beforeEach to clear mock
call history between tests so vi.mock factory fns are reset.

In `@packages/lib/timeFormat.ts`:
- Around line 1-6: The header comment at the top of packages/lib/timeFormat.ts
is outdated (mentions AM/'01 h' string-detection) and should be replaced with a
short, accurate description that the module detects the user's 24h/12h
preference using Intl.DateTimeFormat().resolvedOptions().hourCycle (falling back
to navigator.language or 'en-US' when unavailable) and explains the default
behavior; update the top-of-file comment to reference the hourCycle-based
implementation and navigator.language fallback so future readers aren't misled.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 908b7d8c-88af-416d-af63-5dcf758e38d3

📥 Commits

Reviewing files that changed from the base of the PR and between 987fe91 and 91dfd5c.

📒 Files selected for processing (2)
  • packages/lib/timeFormat.test.ts
  • packages/lib/timeFormat.ts

Comment thread packages/lib/timeFormat.test.ts
Comment thread packages/lib/timeFormat.ts
@romitg2
Copy link
Copy Markdown
Member

romitg2 commented Apr 27, 2026

Thanks for the PR! please add before and after video demo. reopen once it's added.

@romitg2 romitg2 closed this Apr 27, 2026
@Akash504-ai
Copy link
Copy Markdown
Contributor Author

Added demo video and code reference screenshot. Reopening for review.

@Akash504-ai
Copy link
Copy Markdown
Contributor Author

@romitg2 I have added the requested video (demonstrating behavior) along with a code reference screenshot. Could you please reopen the PR for review?

@romitg2 romitg2 reopened this Apr 27, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

This PR has been marked as stale due to inactivity. If you're still working on it or need any help, please let us know or update the PR to keep it active.

@github-actions github-actions Bot added the Stale label May 5, 2026
@romitg2 romitg2 added ready-for-e2e run-ci Approve CI to run for external contributors and removed Stale run-ci Approve CI to run for external contributors labels May 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-e2e run-ci Approve CI to run for external contributors size/M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants