Skip to content

OBPIH-7488 support localized date patterns#5485

Merged
ewaterman merged 5 commits intodevelopfrom
ft/OBPIH-7488-localized-date-patterns
Sep 18, 2025
Merged

OBPIH-7488 support localized date patterns#5485
ewaterman merged 5 commits intodevelopfrom
ft/OBPIH-7488-localized-date-patterns

Conversation

@ewaterman
Copy link
Member

@ewaterman ewaterman commented Sep 8, 2025

✨ Description of Change

Link to GitHub issue or Jira ticket: https://pihemr.atlassian.net/browse/OBPIH-7488

Description: As a part of the timezone fix, we're trying to narrow down to only a specific set of patterns that we use for displaying dates. In addition, we want each of these date formats to be localizable since different languages might display dates differently.

For example, in English we may want to display LocalDate fields like dd/MMM/yyyy but in Chinese we may want to display them like yyyy 年 MM 月 dd 日.

So this ticket does the following:

  • adds the new set of localizable date formats to message.properties
  • fixes a malformatted spanish locale date
  • modifies the existing g:formatDate taglib to support localizing dates
  • modifies the DateFormatterManager.format method to support localizing dates

Ultimately, to localize our fields we do:

  • Instant: DateFormatterManager.format(Instant) which calls TemporalAccessorDateTimeFormatter.format(Instant)
  • LocalDate: DateFormatterManager.format(LocalDate) which calls TemporalAccessorDateTimeFormatter.format(LocalDate)
  • Messages: MessageLocalizer.localize('x.y.z')

📷 Screenshots & Recordings (optional)

Here's a diagram of all the important classes involved:
image

@ewaterman ewaterman self-assigned this Sep 8, 2025
@github-actions github-actions bot added type: feature A new piece of functionality for the app domain: frontend Changes or discussions relating to the frontend UI domain: backend Changes or discussions relating to the backend server domain: l10n Changes or discussions relating to localization & Internationalization labels Sep 8, 2025
default.dashboard.label=Tablero
default.data.label=datos
default.date.format=dd / MMM / aaaa hh\: mm\: ss
default.date.format=dd/MMM/yyyy hh:mm:ss a z
Copy link
Member Author

Choose a reason for hiding this comment

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

while I was testing I noticed that the spanish message for the default date was looking weird. It actually wouldn't parse properly for me. "a" is "AM"/"PM" so I think this format was just wrong...

date.format.date.label=dd/MMM/yyyy
date.format.dateTime.label=dd/MMM/yyyy HH:mm:ss
date.format.dateTimeZone.label=dd/MMM/yyyy HH:mm:ss XXX
date.format.time.label=MM/yyyy
Copy link
Member Author

Choose a reason for hiding this comment

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

These are the localizations for the new date formats. We should only need to override them for locales that want a different format.

But lets discuss what patterns we want to go with. These will display like:

01/Jan/2025
01/Jan/2025 00:00:00
01/Jan/2025 00:00:00 +01:00
00:00:00

Which I picked since that's what we already use throughout most of the app.

.withDisplayFormat(attrs.displayStyle || attrs.format ? null : DateDisplayFormat.GSP)
.build()

return dateFormatterManager.format(attrs.date, context)
Copy link
Member Author

Choose a reason for hiding this comment

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

This allows us to specify additional attrs when using <g:formatDate>. We shouldn't need to use these extra attrs very often but it gives us the flexibility to override behaviours as needed for any edge cases that we have


// Use Grails' namespace because we're overriding the default behaviour and so that the tag lib
// is accessible automatically throughout the app.
static namespace = 'g'
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 might revert this whole class. I wanted to standardize how we localize messages in general but it wasn't working with special characters while I was testing locally. TBD after testing my branch on a deployed environment.

DATE('date.format.date.label', DateTimeFormatter.ofPattern('dd/MMM/yyyy')),
DATE_TIME('date.format.dateTime.label', DateTimeFormatter.ofPattern('dd/MMM/yyyy HH:mm:ss')),
DATE_TIME_ZONE('date.format.dateTimeZone.label', DateTimeFormatter.ofPattern('dd/MMM/yyyy HH:mm:ss XXX')),
TIME('date.format.time.label', DateTimeFormatter.ofPattern('HH:mm:ss'))
Copy link
Member Author

Choose a reason for hiding this comment

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

The enum version of the dates in message.properties. We don't strictly need this class but I find it much nicer to be able to work with enums instead of hard coded strings. That way if we need to change the underlying labels we can do so without refactoring all the usages

* Context object containing the configuration fields for formatting dates.
* For a majority of cases the default settings can be used and so this context object will not be required.
*/
class DateFormatterContext {
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 just moved this class to its own file. The only addition is displayStyleOverride

* A simple wrapper class on a translatable message/label.
* Used to indicate that translations can be applied to the message.
*/
class LocalizableMessage {
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 didn't end up using this class but I would like to keep it in since I eventually want to do some work towards standardizing our error messages and this would play a part in that. I'm imagining we have either a custom error class or a custom error message class that can hold a localized message and any other data we need.

* Translates messages into a specific locale.
*/
@Component
class MessageLocalizer {
Copy link
Member Author

Choose a reason for hiding this comment

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

This is meant to standardize how we localize our messages in the app. We have a ton of different approaches and I didn't know which to choose when localizing dates for this PR so got fed up and decided to try to unify them all under one component. Eventually I'd like to unify our errors and GSPs to all localize via the same method so that we can remove the other approaches. That's a refactoring I'll save for a future date but this at least gives us a starting point

* If you're trying to translate a message, use {@link org.pih.warehouse.core.localization.MessageLocalizer}.
*/
@Component
class LocaleDeterminer { // Note I wanted to call this LocaleResolver but that's already a Spring component
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 noticed we had a ton of different ways that we resolved locales so I decided that I was going to bite the bullet and try to unify our behaviour.

I'll eventually add a flow diagram to this PR to demonstrate all the components that go into this change since I feel like it's hard to visualize them all. I figured a bunch of single purpose classes gives us better flexibility and testability than a single, massive class that does locale, localization, and messaging all in one place.

Eventually I want to refactor LocalizationUtil and all the other l10n related services as well but that'd be too much for one PR


import org.pih.warehouse.core.localization.MessageLocalizer

class LocalizationTagLib {
Copy link
Member Author

Choose a reason for hiding this comment

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

This isn't currently used anywhere. I figured we'd want to experiment with the new messageLocalizer before applying it everywhere. Once we're comfortable that this flow works, we can switch the namespace of this class to 'g' and then everywhere that we have <g:message> will go through this taglib.

@ewaterman ewaterman merged commit 01b2e5c into develop Sep 18, 2025
2 of 5 checks passed
@ewaterman ewaterman deleted the ft/OBPIH-7488-localized-date-patterns branch September 18, 2025 21:23
date.format.date.label=dd/MMM/yyyy
date.format.dateTime.label=dd/MMM/yyyy HH:mm:ss
date.format.dateTimeZone.label=dd/MMM/yyyy HH:mm:ss XXX
date.format.time.label=HH:mm:ss
Copy link
Member

@jmiranda jmiranda Sep 18, 2025

Choose a reason for hiding this comment

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

As i mentioned on the call last week (?), i would really like to keep these all under the default namespace in messages.properties.

default.date.format=dd/MMM/yyyy
default.time.format=HH:mm:ss
default.date.time.format=dd/MMM/yyyy HH:mm:ss
default.date.time.timezone.format=dd/MMM/yyyy HH:mm:ss XXX

If that's going to cause too big of a change (since default.date.format has always been used to render the full datetime format) then we could preserve that by leaving it alone

default.date.format=dd/MMM/yyyy hh:mm:ss a z

and adding more context-specific formats

default.date.only.format=dd/MMM/yyy
default.datetime.format=dd/MMM/yyyy HH:mm:ss
default.datetime.timezone.format=dd/MMM/yyyy hh:mm:ss a z
default.time.format=HH:mm:ss

and then adding more date formats in the future, as needed if codifying these became necessary.

default.date.expiry.format=MMM yyyy
default.date.pretty.format=EEE, MMM dd, yyyy
default.date.weekday.format=EEE
default.date.mysql.format=yyyy-MM-dd
default.date.filename.format.=yyyyMMdd
default.datetime.filename.format=yyyyMMddHHmmss

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

Labels

domain: backend Changes or discussions relating to the backend server domain: frontend Changes or discussions relating to the frontend UI domain: l10n Changes or discussions relating to localization & Internationalization type: feature A new piece of functionality for the app

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants