feat: date time formats from locales
Fixes https://github.com/flarum-lang/chinese-simplified/issues/23 and https://github.com/flarum-lang/chinese-simplified/issues/27 (in another way)
Changes proposed in this pull request:
This PR adds some time formats to dayjs, which makes the customized time formats used by Flarum localizable in locale object of dayjs locales.
This PR makes Flarum formats all date time using the formats from the language packs.
The PR also made some improvements to humanTime and liveHumanTimes's comments.
Reviewers should focus on:
The new formats' naming and whether is it a good implementation.
All language packs should contain the following formats after this is merged:
-
fandF: ForhumanTime -
ffandFF: For scrubber
It's not necessary though, a fallback will be used if they are not provided.
Check if all date time are formatted using the format from the language pack.
Although I've tested on my local Flarum installation, it would be nice for you to test it again.
Screenshot
QA
All places where humanTime is used and post stream's scrubber.
Modify date time formats in locale and see if it works.
Necessity
- [ ] Has the problem that is being solved here been clearly explained?
- [ ] If applicable, have various options for solving this problem been considered?
- [ ] For core PRs, does this need to be in core, or could it be in an extension?
- [ ] Are we willing to maintain this for years / potentially forever?
Confirmed
- [x] Frontend changes: tested on a local Flarum installation.
- [ ] Backend changes: tests are green (run
composer test). - [ ] Core developer confirmed locally this works as intended.
- [ ] Tests have been added, or are not appropriate here.
Required changes:
- [ ] Related documentation PR: (Remove if irrelevant)
- [ ] Related core extension PRs: (Remove if irrelevant)
I have another question, because we are adding features in Flarum core, can we make this feature more configurable?
such as
> new Intl.DateTimeFormat('zh-TW-u-ca-roc', { year: 'numeric', month: 'long', day: 'numeric', }).format(new Date(2025, 4, 14));
'民國114年5月14日'
so I prefer that we make a flarum extender thus language-pack or something else plugin can define a new format.
so I prefer that we make a flarum extender thus language-pack or something else plugin can define a new format.
This is possible with the version in my working tree. But I don't see the possible use cases.
https://x.com/taltalasuka/status/1839500307094450552
fyi
I have another question, because we are adding features in Flarum core, can we make this feature more configurable?
such as
> new Intl.DateTimeFormat('zh-TW-u-ca-roc', { year: 'numeric', month: 'long', day: 'numeric', }).format(new Date(2025, 4, 14)); '民國114年5月14日'so I prefer that we make a flarum extender thus language-pack or something else plugin can define a new format.
I see what you mean, the problem is that this PR only solves the hardcoded format problem with Flarum and DayJS's Localized Format plugin. The feature you requested needs a big change to how Flarum and DayJS work.
make sense
Note that there was some discussion about it in: https://discuss.flarum.org/d/22913-what-date-format-do-you-use
As for the problem, I would much more prefer format defined as part of forum translation, like:
ago = d.format(app.translator.trans('core.forum.date-format.humanTimeShort'));
It will be easier to understand and maintain for language pack maintainers, and you can quite easily adjust it also as a forum owner. And overall it seems to be simpler and less hacky solution than introducing some cryptic FF formats.
As for the problem, I would much more prefer format defined as part of forum translation
I think this is kinda messy since you have one set of formats in DayJS that cannot be easily edited by forum owners and another set of formats in Flarum locale system.
This is the reason I didn't implement it in this way at the beginning.
I think this is kinda messy since you have one set of formats in DayJS that cannot be easily edited by forum owners and another set of formats in Flarum locale system.
I'm not sure what the problem is. Most language packs copied dayjs translations from the source without any changes (I actually had plans to automate it and fetch updates automatically from dayjs repo). This format can be quite cryptic and tricky to change, but there is no really other way to translate dayjs.
If your solution relies on editing dayjs translations, then it inherits all its problems: it is more problematic for language pack maintainers and much harder to override by forum owners. On the other hand, if we use app.translator.trans() to pick format, then adjustring in Weblate or using Linguist should be quite easy. And in the future Flarum could add option to override these formats in admin panel (it could just override specific translations from language pack).
On the other hand, if we use
app.translator.trans()to pick format
Actually, my latest code on my working tree already removed the Localized Format plugin from DayJS, so it's possible to make it also use the formats available in the language pack instead of the one from DayJS locale.
In this way, the priority of the format can become like this:
Language pack (Flarum translator) > DayJS locale > Hardcoded default English format
So we can make all formats available in DayJS Localized Format plugin and the ones that Flarum uses editable in Flarum's translator.
Does this sound good to you?
Some details of the changes made still need some discussion, the PR is now a draft.
@rob006 I've pushed new changes, would you like to check if it looks good to you? I haven't tested it yet.
Is there any reason why you implemented this in this way instead of passing app.translator.trans() result to format()?
Is there any reason why you implemented this in this way instead of passing
app.translator.trans()result toformat()?
I just did what DayJS did to implement the localized format and make it work with Flarum. Also made it extendable.
@SychO9 I think it would be much more practical and intuitive to override/translate format used in specific situation/place, than just overwrite dayjs formats. For example in Polish language pack I would prefer to use LL instead of ll in humanTime(), but it does not mean that I want to overwrite ll everywhere.
Also, from translator perspective, it is much easier to understand key like core.lib.datetime_formats.humanTimeFull than core.lib.datetime_formats.LL. And spaces and special characters in translations keys could be a can of worms...
@YUCLing Sorry, it looks overcomplicated to me and I don't see a practical reason to implement this in that way.
@rob006 that's a fair point. I guess context matters more in this case. I was trying to avoid having to define that every time, but that would force the same custom format everywhere.
In that case I think we can proceed with context. Depending on where the datetime is used, we can add a locale key to define the format. So humanTime formats can be under core.lib.datetime_formats
Another example is one used in poststream MMMM YYYY that can probably then go under core.forum.post_scrubber.description_format
so same proposition as above, except the format parameter will be the locale key, and we need to add a fallback format value. The method can then take care of the edge case where the locale is not available (returns the key used instead of a format)
Alternative PR that implements the idea from https://github.com/flarum/framework/pull/4029#issuecomment-2381297643: https://github.com/flarum/framework/pull/4053
Thanks for the PR! much appreciated!
I think the FF ff formats with the dayjs plugin will only add to the confusion with date time formats and future maintenance.
I'm going to propose something a little more simple but more clear.
Let's add a
format(format: string, datetime: Dayjs)to theTranslatorclass in common.this method would:
- check if the provided format exists in a custom item list (
customFormats()) and use that.- Fallback to
app.translator.trans('core.lib.datetime_formats.'+format)- Fallback to the actual format.
There are a lot of different formats used throughout the app, (+extensions) so this would give language packs more power over how those formats are changed.
The locale would be literally the format as a key, so:
core: lib: datetime_formats: "h A": "h A" "MMMM YYYY": "MMMM YYYY" "YYYY-MM-DD": "YYYY-MM-DD" "YYYY-MM-DD h:mm A": "YYYY-MM-DD h:mm A" "YYYY-MM-DD h:mm:ss A": "YYYY-MM-DD h:mm:ss A" "LL": "LL" "LLL": "LLL" "LLLL": "LLLL"core would have one single example commented out. While language packs can fill more entries as needed.
The new method would be called like so:
app.translator.format('MMMM YYYY', datetime)@YUCLing @rob006 what do you think? do we see any potential issues with that approach?
Passing the dayjs object to the translation plugins or letting the translation plugins decide how to convert the date time is a good idea.
Because it can also make it possible to use another calendar like zh-TW-u-ca-roc or something else like Japanese and Chinese Lunar Calendar.
The fallback logic is also a good compatibility to the existing translation plugins. It can also let most existing languages and cultures to use these features without custom formatter.
Though it is rare to use these unusual calendars except the Gregorian calendar, It should be considered when refactoring the date format feature.
app.translator.format('MMMM YYYY', datetime)
I've implemented it in this way, what do you guys think?
@SychO9 @ExerciseBook Check if the latest changes still contain issues 😃
What is the use case for dateTimeFormats and fallback?: string parameter?
@YUCLing awesome work!
What is the use case for dateTimeFormats and fallback?: string parameter?
- dateTimeFormats is for extensibility, it allows extensions to add custom behavior.
- the fallback is meant to be used in case for whatever reason the locale value is not available so it won't produce an erroneous value. Language packs might not have it right away. Though I wonder if we really need it.
- dateTimeFormats is for extensibility, it allows extensions to add custom behavior.
I'm asking for some practical example. I'm not sure how and when extension should use it over overriding specific translation key.
- the fallback is meant to be used in case for whatever reason the locale value is not available so it won't produce an erroneous value. Language packs might not have it right away. Though I wonder if we really need it.
If language pack do not have specific key, then English translation will be used. AFAIK such fallback will be used only when extension will call formatDateTime() with key that is intentionally not defined in YAML file. But this is problematic for translators - Weblate won't detect this as phrase to translate and most translators will not be aware that they can adjust it. I don't think we should promote such usage of formatDateTime(), and I'm not sure why would you want to use fallback over defining translation key in YAML file.
If language pack do not have specific key, then English translation will be used.
Ah I see, I wasn't actually aware of that. Then yeah we can do without the fallback parameter indeed.
I'm asking for some practical example. I'm not sure how and when extension should use it over overriding specific translation key.
For example, this would allow building an extension that uses admin level defined formats or even user preference formats to use as a first priority. Then fallback to the default behavior which is locales.
For example, this would allow building an extension that uses admin level defined formats or even user preference formats to use as a first priority. Then fallback to the default behavior which is locales.
But these formats should be localized. I assume that such extension would have predefined formats defined as translations (so these could be translated by language packs). So this would result formatDateTime() call inside of formatCallback?
But these formats should be localized. I assume that such extension would have predefined formats defined as translations
No the use case would involve raw formats user-defined. Either through text or selection fields. The specifics would be up to the extension/developer's use case.
@SychO9 So if I have forum that supports 10 languages, then I should have separate settings for these 10 languages in admin panel?
@rob006 if you want an extension like that, that's up to you. Not every forum needs more than 1 language, so for many it's a valid use case.
Point is, this is for custom behavior. Up to the developer/webmaster to determine what they want, up to the dev to make it happen. We just enable proper ways of extending behavior/components. Rather than leave things to hacking up the code. This enables us to make changes to the core code without worrying constantly about breaking backwards compatibility.
@SychO9 My point is: if we don't know exactly how it is supposed to be used, then maybe adding an API to cover use case we don't fully understand may be a dead end and will require some adjustments in the future (this may result a BC break). It is already possible to adjust date formats by overriding translations, so you don't even need dedicated extension, you can use Linguist for that. So dateTimeFormats is required to cover some edge case we don't know right now?
Personally I don't like that it do not involve currently used locale - it is easy to forget the fact that formats may differ for different languages. Passing locale to formatCallback() would at least suggest to this is something you should care about.
@YUCLing Can we have this PR against 1.x branch?
OK, I checked new implementation and now I'm even more confused. I assumed that formatCallback() will get translation key (formatCallback('core.lib.datetime_formats.human_time_short')), so it could be used to overwrite translation for specific context. But it looks like it gets format taken from translation (formatCallback('ll')) - I have no idea what is practical use case for this.
@rob006 but I gave you a simple valid use case. For the user to be able to fill the formats themselves as a preference. I understand it is not something you personally see the point in. This is something I can do in other forum software as well and isn't all that odd.
Also, even without this a developer could still make it happen. But by modifying parts of the code that we consider private API. What we add here is a public API where even if we make changes to the code in the future, allows us to prevent BC breaks by handling the item list as necessary.