chore: deprecate Intl wrapper helpers and document removal#2386
chore: deprecate Intl wrapper helpers and document removal#2386andrii-bodnar merged 25 commits intolingui:nextfrom
Conversation
|
@yslpn is attempting to deploy a commit to the Crowdin Team on Vercel. A member of the Team first needs to authorize it. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2386 +/- ##
==========================================
+ Coverage 77.05% 81.27% +4.22%
==========================================
Files 84 104 +20
Lines 2157 2718 +561
Branches 555 724 +169
==========================================
+ Hits 1662 2209 +547
+ Misses 382 363 -19
- Partials 113 146 +33 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
Hey @yslpn thanks for the contribution. I'm not sure that developers would have enough time to transition and that will not block adoption ESM only linguijs. So it would be better to not remove it in the next major version. Blog post would be much apreciated to explain what was the driver for the decision. Meanwhile, do you think it would be nice to have ready to use react hooks as an alternative? Because users will still have to create them on their own, may be we can create them and make available out of the box? |
If we ship ready-made React hooks for date/number formatting, people will soon want the same helpers outside React, and we end up back where we were: maintaining wrappers around every Does that sound good to you?
I also drafted a blog post to explain the deprecation and migration; would love your take on it. The social image in the post was generated via AI (Nano Banana by Gemini). |
|
I’ll be honest with you: this PR has been sitting around because the blog post feels very LLM-ish, and I don’t really want to merge something like that. I also don’t have enough energy to rewrite it myself, so I’ve been kind of dodging this PR. Could you rewrite it in a more human way — how you would explain what changed and why — without going into too much detail? Ideally, it should be engaging to read and feel less like it was written by a robot. |
Yes, I used LLM when writing it. I'll mark the PR as a draft and rewrite it when I'm ready. |
vonovak
left a comment
There was a problem hiding this comment.
I added a few suggestions. We should revisit especiially the "Migrating to native Intl" section.
But thanks for the work!
| function getDateFormatter(options?: Intl.DateTimeFormatOptions) { | ||
| const locales = i18n.locales ?? i18n.locale; | ||
| return new Intl.DateTimeFormat(locales, options); | ||
| } | ||
|
|
||
| function getNumberFormatter(options?: Intl.NumberFormatOptions) { | ||
| const locales = i18n.locales ?? i18n.locale; | ||
| return new Intl.NumberFormat(locales, options); | ||
| } | ||
|
|
||
| export function formatOrderSummary(date: Date, total: number) { | ||
| const dateFormatter = getDateFormatter({ dateStyle: "medium" }); | ||
| const numberFormatter = getNumberFormatter({ | ||
| style: "currency", | ||
| currency: "EUR", | ||
| }); | ||
|
|
||
| return `${dateFormatter.format(date)} - ${numberFormatter.format(total)}`; | ||
| } |
There was a problem hiding this comment.
This suggested code has two problems:
- it assumes that
i18ninstance comes from@lingui/core. We should recommend code that is agnostic of thei18ninstance origin. It could come from@lingui/core, fromreact contextor other place. We should not assume. - it's suboptimal because for each
formatOrderSummarywe create a new instance of theIntlobjects. Let's aim to not create new objects if not really needed.
this is a sketch:
function getDateFormatter(locales: Locales | undefined) {
const options: Intl.DateTimeFormatOptions = {
year: "numeric",
month: "long",
day: "numeric",
}
return new Intl.DateTimeFormat(locales, options);
}
const memoizedGetDateFormatter: typeof getDateFormatter = memoize(getDateFormatter);
function getNumberFormatter(locales: Locales | undefined, options?: Intl.NumberFormatOptions) {
return new Intl.NumberFormat(locales, options);
}
export function formatOrderSummary(i18n: I18n, date: Date, total: number) {
const locales = i18n.locales ?? i18n.locale;
const dateFormatter = memoizedGetDateFormatter(locales);
// note - unlike memoizedGetDateFormatter, getNumberFormatter creates a new Intl instance on each call!
// consider memoizing it if formatter options are stable
const numberFormatter = getNumberFormatter(locales, {
style: "currency",
currency: "EUR",
});
return `${dateFormatter.format(date)} - ${numberFormatter.format(total)}`;
}
function useDateFormatter() {
const { i18n } = useLingui();
const locales = i18n.locales ?? i18n.locale;
return memoizedGetDateFormatter(locales);
}
There was a problem hiding this comment.
We'll be accepting an i18 instance from the outside. I've added this to my examples.
There's no need to delve too deeply into premature optimization. Library users can decide if they need to.
I think creating formatting functions will be a simple operation and won't cause any performance issues.
| [] | ||
| ); | ||
|
|
||
| const dateFormatter = useDateFormatter(dateOptions); |
There was a problem hiding this comment.
if you have a hundred PriceLines rendered, that's
- 100
dateOptions - 100
numberOptions - 100
Intl.NumberFormats - 100
Intl.DateTimeFormats
Where you could have 1 of each and it'd work the same - I know this may be just a contrived example, but it adds up and people tend to copy the examples.
There was a problem hiding this comment.
I've extracted the options from React, but to keep the example simple, I suggest creating formatting functions for each component instance.
Adding unnecessary complexity can confuse users. They might think they have to optimize this, but I'm not so sure.
|
|
||
| - Now: mark the helpers as deprecated in code and docs without breaking existing apps. | ||
| - Upcoming majors: keep them long enough to give teams time to migrate; removal will happen in one of the next majors (not necessarily the very next one). | ||
| - Removal: no separate warning blog is planned - please track the release notes for major versions. |
There was a problem hiding this comment.
We should think about offering a codemod, or asking community to contribute a codemod.
There was a problem hiding this comment.
It seems like it would be difficult to make any kind of reliable codemod here.
Co-authored-by: Vojtech Novak <vonovak@gmail.com>
…ion from Intl wrappers
…native Intl methods
|
Hi @yslpn, thanks for the contribution! I would suggest retargeting this PR to the |
@andrii-bodnar Good idea. You can switch the target branch to the next one. There shouldn't be any conflicts. Should I delete the blog post or do something else from my side? |
|
Hi @vonovak Can I ask you to review this again please? I made some changes. The most significant ones are:
Was: new Intl.NumberFormat(i18n.locales ?? i18n.locale).format(12345.678);Now: new Intl.NumberFormat(i18n.locale).format(12345.678);The old version didn't make sense because NumberFormat doesn't throw an error and will always find some locale if the one passed is invalid. Main backup en-US
|
Description
Deprecate Lingui’s Intl wrappers (
i18n.date/numberplus the shared date/time/number helpers) ahead of removal, and add documentation callouts to direct users to nativeIntl.DateTimeFormat/Intl.NumberFormat. Related: #2265.Open question for @timofei-iatsenko: do we also want a short blog for this deprecation, and is it OK to plan removal in the next major given the ESM-only focus discussed in #2363? Or it would be better to change the deprecated message.
Background
Initially, Lingui aimed to be a Swiss Army knife for localization, trying to take on as much as possible: from DX to performance optimization. We strived to create convenient formatting wrappers with built-in memoization, normalization, and so on. Now we have functions like
i18n.dateandi18n.number. But this prevents us from moving forward, and we need to abandon them.Problems
Creating such wrappers around native APIs leads to the following problems:
This is additional code that needs to be supported and maintained. The library does too much. We need to do less, but do it well. Lingui JS should focus on message extraction and working with catalogs. Formatting dates, numbers, and so on is the job of the
IntlAPI.These wrappers increase bundle size. Not all users use them. For example, there may be applications where date formatting doesn't depend on locale and is configured manually by the user. Tree-shaking doesn't work here because these are class methods.
We can't create wrappers for all Intl methods—there are too many of them https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl. This leads to poor DX when it's unclear what's in the library and what's not. This is exactly what prompted me to create an issue when I couldn't find how to use Intl.NumberFormat in Lingui.
Plan
In a minor release, we're publishing a blog post, updating documentation, and adding deprecated warnings for methods via JSDoc comments.
In one of the major releases, we will remove the methods, but before that, we'll give enough time for users to prepare their code.
Migrating to native Intl
The migration principle is simple: we need to replace the use of
i18n.dateandi18n.numbermethods with direct use ofIntl.DateTimeFormatandIntl.NumberFormatrespectively.i18n.date(date, options)=>new Intl.DateTimeFormat(i18n.locale, options).format(date);i18n.number(n, options)=>new Intl.NumberFormat(i18n.locale, options).format(n);Example without React:
In React you have to use
useMemoto memoize formatters and react to locale changes:Types of changes
Fixes #2265
Checklist