Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: rtl date time format blog #7936

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
Binary file not shown.
33 changes: 33 additions & 0 deletions packages/dev/docs/pages/assets/rtl-actual-placeholder.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/dev/docs/pages/assets/rtl-keyboard.mp4
Binary file not shown.
33 changes: 33 additions & 0 deletions packages/dev/docs/pages/assets/rtl-timefield.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
140 changes: 140 additions & 0 deletions packages/dev/docs/pages/blog/rtl-date-time.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
{/* Copyright 2025 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License. */}

import {BlogPostLayout, Video, Track, Image} from '@react-spectrum/docs';
export default BlogPostLayout;

import RTLTimefield from '../assets/rtl-timefield.svg';
import RTLActualPlaceholder from '../assets/rtl-actual-placeholder.svg';
import localeVideoURL from 'url:../assets/datefield-locales.mp4';
import placeholderVideoURL from 'url:../assets/datefield-placeholder.mp4';
import keyboardVideoURL from 'url:../assets/rtl-keyboard.mp4';

---
keywords: [date picker, date, time, calendar, components, accessibility, react spectrum, react, spectrum]
description: Internationalization is a core feature of our Date and Time components. We support 13 different calendar systems as well as locale-specific formatting, number systems, and 12 and 24 hour time. However, we identified an issue with our right-to-left support where in some right-to-left (RTL) languages, the format of the date and time fields was incorrect. While investigating this bug, we faced several challenges in ensuring proper date and time representation in RTL languages and implemented various strategies that we’d like to share.

date: 2025-03-30
author: '[Yihui Liao](https://github.com/yihuiliao)'
---

# Improving Internationalization Support in Our Date and Time Components

Internationalization is a core feature of our Date and Time components. We support 13 different calendar systems as well as locale-specific formatting, number systems, and 12 and 24 hour time. However, we identified an issue in our support for several right-to-left (RTL) languages where in some right-to-left (RTL) languages, the format of the date and time fields was incorrect. While investigating this bug, we faced several challenges in ensuring accurate date and time representation in RTL languages and implemented various strategies that we’d like to share.


## The Structure of Our Date and Time Components

In a [previous blog post](https://react-spectrum.adobe.com/blog/date-and-time-pickers-for-all.html#date-fields), we discussed the reasoning behind the component structure of our date and time components. In short, we designed these components to render individually focusable segments for each date and time unit, eliminating the challenges of parsing various date formats — an issue commonly encountered with free-form text fields. Since the date and time format is automatically determined based on the user’s locale, the user only needs to fill in the values without worrying about the appropriate separators or the order. This made for a smoother, more intuitive experience for the user, removing the guesswork associated with formatting and parsing dates in various locales.


<Video src={localeVideoURL} loop autoPlay muted />

## Unicode Bidirectional Algorithm

To format the segments according to the user locale, we rely on the browser’s [Unicode Bidirectional Algorithm](https://unicode.org/reports/tr9/). However, we found that some of our CSS styles were interfering with the algorithm's application, leading to incorrect formatting. For instance, in Hebrew (`he-IL`), the proper numeric date format should be `DD.MM.YYYY`, but our date component was displaying `YYYY.MM.DD`. This issue varied across different RTL languages for date fields, but for time fields, we observed a consistent problem across all RTL languages where time segments were flipped, rendering `MM:HH` instead of the correct `HH:MM` format.
Copy link
Member

Choose a reason for hiding this comment

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

Previous to the change, I wouldn't say we were relying on the unicode bidi algorithm, we were relying on flex box. Maybe it would make sense to say we rely on the browsers Intl.DateTimeFormat API and then introduce the bidi algorithm later when you talk about the fix?


<RTLTimefield />
Copy link
Member Author

Choose a reason for hiding this comment

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

i have some things i'd like to fix this image so it'll get eventually get replaced but the jist is there. wanted to have something so people could see how the blog flows with this image


We found the culprit to be two things. First, we were applying `display: flex` on the container wrapping the segments. Second, each segment was being rendered as a `div` with `display: block`. Instead, we needed to use [normal CSS flow layout](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_display/Flow_layout) on the wrapper around the segments and update each segment to be a span instead.
Copy link
Member

Choose a reason for hiding this comment

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

Maybe add something like this after the sentence about flex?

Usually, this is the correct way to handle right-to-left layout since it automatically mirrors in those languages. However, in this case, the segments are not strictly right-to-left or left-to-right: they are a mix.

I think the display: block issue is really more of a side effect of the change to using flow layout. When we used flex this didn't matter, but it did once we used flow layout. Probably less important to mention than the flex part.

At the end of the paragraph seems like a good point to introduce Unicode bidi.

Instead, we needed to use normal CSS flow layout on the wrapper around the segments, an update each segment to be a span instead of a div. This allows the browser to apply the Unicode Bidirectional Algorithm, which reorders the characters in right-to-left text according to a set of standardized rules.


While it seemed like a relatively simple fix, we later discovered through testing that this only corrected the format when segments contained actual values. If they had placeholder values, the order was still incorrect, causing some undesirable behaviors. It seemed that the Unicode Bidirectional Algorithm was interpreting placeholder values differently from actual values. As a result, when a segment was cleared back to its placeholder, it would shift back to the incorrect order. Furthermore, when a user entered a value, the segment would shift back to its correct order. This posed an interesting challenge: how do we ensure consistent formatting regardless of whether a segment contained a placeholder or a user-entered value — all without hard coding segment order for each locale?
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
While it seemed like a relatively simple fix, we later discovered through testing that this only corrected the format when segments contained actual values. If they had placeholder values, the order was still incorrect, causing some undesirable behaviors. It seemed that the Unicode Bidirectional Algorithm was interpreting placeholder values differently from actual values. As a result, when a segment was cleared back to its placeholder, it would shift back to the incorrect order. Furthermore, when a user entered a value, the segment would shift back to its correct order. This posed an interesting challenge: how do we ensure consistent formatting regardless of whether a segment contained a placeholder or a user-entered value — all without hard coding segment order for each locale?
While it seemed like a relatively simple fix, we later discovered through testing that this only corrected the format when segments contained numeric values. If they had placeholder values, the order was still incorrect, causing some undesirable behaviors. This was because the placeholders are typically non-numeric characters, e.g. `'شهر'` representing "month" in Arabic. As a result, when the user pressed the Backspace key to clear a segment to its placeholder, the order of the segments would change. Furthermore, when a user entered a value, the segment would shift back to its correct order. This posed an interesting challenge: how do we ensure consistent formatting regardless of whether a segment contained a placeholder or a user-entered value — all without hard coding segment order for each locale?


Below is an example of how the dates were being formatted based on user-entered or placeholder values along with a video to demonstrate the shifting behavior:

<RTLActualPlaceholder />

<Video src={placeholderVideoURL} loop autoPlay muted />
Copy link
Member

Choose a reason for hiding this comment

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

Might just be me, but I kinda found it hard to notice what this video was illustrating due to my unfamiliarity with the numerals here. Maybe an annotated before and after image would be more helpful?

Copy link
Member Author

Choose a reason for hiding this comment

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

ah yeah i can see how that's confusing. i wanted the video to showcase how it's confusing for the segments to shift around but maybe i can also insert images as well that are annotated to explain. i'll play around with it a bit

Copy link
Member

Choose a reason for hiding this comment

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

I think we should make the videos not loop and not autoplay, and just give the user play controls. It's hard to see where the video begins and ends so a bit challenging to tell what's happening.



## TimeFields

We first addressed time fields since they were easier to fix. As mentioned earlier, the segments in time fields for RTL languages were flipped. We learned, however, that regardless of locale, all time fields should follow the `HH:MM` format. Knowing this, we could apply a direction of left-to-right (LTR) on the numeric values across all segments in a time field.

Instead of wrapping the the segments in a `<bdo>` tag with a `dir=“ltr”` which would impact the DOM structure and have potentially introduce side effects, we chose to use the [LRI (left-to-right isolate) Unicode character](https://www.w3.org/International/questions/qa-bidi-unicode-controls) to encapsulate the time segments and force an LTR direction. Adding this Unicode character is the equivalent of wrapping the time segments in a `<bdo>` tag but offers several advantages. Since the character is invisible, there are no visual changes, and by adding it as a siblings to the segments, we avoided major structural changes to the DOM. Additionally, by enforcing a LTR direction, we no longer had to worry about whether the time field consisted of placeholders or actual values. Lastly, it ensured that when a date field included a time, that the time field appeared in the correct order with respect to the date field (e.g. `8:45 1/31/2025` instead of `1/31/2025 8:45`).
Copy link
Member

Choose a reason for hiding this comment

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

Link to bdo on MDN? I bet most developers haven't used it


Below is a simplified code example of how we utilize Unicode characters to enforce an left-to-right direction on the segments:

```tsx example render=false
<div styles={{display: 'inline'}}>
{/*- begin highlight -*/}
<span>{'\u2066'}</span>
{/*- end highlight -*/}
<span aria-label="hour">2</span>
<span>:</span>
<span aria-label="minute">45</span>
{/*- begin highlight -*/}
<span>{'\u2069'}</span>
{/*- end highlight -*/}
</div>
```

## DateFields

Date fields, on the other hand, were much more complicated to solve in comparsion. Since the Unicode Bidirectional algorithm was not formatting the segments, the resulting format appeared to mirror the order in which the segments were stored, as returned by [DateFormatter](https://react-spectrum.adobe.com/internationalized/date/DateFormatter.html#dateformatter). This suggested that we could apply a similar approach to what we used for time fields — forcing a left-to-right direction on the date segments. However, this assumption proved too broad. In some locales, such as Arabic (`ar-AE`), the date segments were already correctly formatted, meaning that enforcing a left-to-right direction would make it incorrect. We found that in Arabic, the separators between date segments contained [right-to-left marks](https://en.wikipedia.org/wiki/Implicit_directional_marks) which was the reasoning behind why the date segments were already correctly formatted. In contrast, Hebrew did not have such markers. Therefore, we had to adopt a different approach that accounted for these variations.


```tsx example render=false
// An example of a date field in ar-AE
<div>
<span aria-label="day">3</span>
{/*- begin highlight -*/}
<span>&rlm;/</span>
{/*- end highlight -*/}
<span aria-label="month">11</span>
{/*- begin highlight -*/}
<span>&rlm;/</span>
{/*- end highlight -*/}
<span aria-label="year">2020</span>
</div>
```

```tsx example render=false
// An example of a date field in he-IL
<div>
<span aria-label="day">3</span>
<span>.</span>
<span aria-label="month">11</span>
<span>.</span>
<span aria-label="year">2020</span>
</div>
```

Through much trial and error, we discovered that appplying the [left-to-right embedding (LRE) Unicode](https://unicode.org/reports/tr9/#Explicit_Directional_Embeddings) on the date segments allowed us to to treat the text as embedded left-to-right while preserving the right-to-left mark on the separators, ensuring that Arabic dates display in the correct format. While we could have added Unicode to the segments like we did with the time fields, we opted for the [equivalent CSS](https://unicode.org/reports/tr9/#Markup_And_Formatting) approach instead to avoid modifying the DOM. This CSS is applied on date segments with placeholder or actual values to avoid the behavior discussed earlier with shifting segments. Through additional testing, we found that we should only apply left-to-right embedding on numeric values. If the value was displayed as text (e.g. "November" instead of "11") we did not apply this CSS.
Copy link
Member

Choose a reason for hiding this comment

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

We might need to show how it was rendering in the incorrect cases again here. I lost track of which problem this was solving when reading it. If I remember correctly it was so that the segments were ordered in LTR but the contents of the individual segments were RTL (so as not to mess up the placeholder text)?


## Keyboard Navigation

After fixing the formatting, we also needed to update the keyboard navigation. Previously, when pressing the left arrow key, you would go to the next node in the DOM and vice versa for the right arrow key. However, after these changes, visually adjacent elements were not necessarily adjacent in the DOM so this would not work anymore. For example, in Hebrew (`he-IL`), the day and minute segment are supposed to be visually adjacent (e.g. `HH:MM DD.MM.YYYY`), but in the DOM, there are several other segments between them.

```tsx example render=false
// An example of the DOM structure of a date field in he-IL
<div>
<span aria-label="day">3</span>
<span>.</span>
<span aria-label="month">11</span>
<span>.</span>
<span aria-label="year">2020</span>
<span>,</span>
<span aria-label="hour">8</span>
<span>:</span>
<span aria-label="minute">45</span>
</div>
```

Below is an example of a date field in Hebrew with the correct date format but incorrect keyboard navigation. Pressing the left arrow key should navigate you to the segment to the immediate left, while the right arrow key should navigate you to the segment to the immediate right.

<Video src={keyboardVideoURL} loop autoPlay muted />

As a result, we updated the keyboard navigation in right-to-left langauges to calculate the distance between the currently focused segment and other segments to identify the closest node based on whether the left or right arrow key was pressed, rather than reying on the DOM order.

## Conclusion

As you can see, formatting dates correctly is quite challenging, especially in right-to-left languages. Fortunately, tools like the Unicode Bidirectional Algorithm help with formatting so we don’t have to handle everything manually. However, as we discovered with this bug, it doesn’t always work as expected and unexpected factors can interfere with it.

After extensive testing, we developed a solution that ensures proper formatting for users of React Spectrum, React Aria Components, and our hooks. If you haven’t tried our date and time components yet, we encourage you to check them out! And if you’re already using them, be sure to update to at least version ^3.40 to ensure correct formatting in right-to-left languages and to make the appropriate changes noted in the [release notes](https://react-spectrum.adobe.com/releases/2025-03-05.html#date-and-time-formatting-in-rtl-languages)!
Copy link
Member

Choose a reason for hiding this comment

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

3.40 only applies to RSP. Maybe just say the March 2025 release?