Skip to content

chore: deprecate Intl wrapper helpers and document removal#2386

Merged
andrii-bodnar merged 25 commits intolingui:nextfrom
yslpn:chore/deprecate-intl-wrappers
Feb 13, 2026
Merged

chore: deprecate Intl wrapper helpers and document removal#2386
andrii-bodnar merged 25 commits intolingui:nextfrom
yslpn:chore/deprecate-intl-wrappers

Conversation

@yslpn
Copy link
Contributor

@yslpn yslpn commented Dec 16, 2025

Description

Deprecate Lingui’s Intl wrappers (i18n.date/number plus the shared date/time/number helpers) ahead of removal, and add documentation callouts to direct users to native Intl.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.date and i18n.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 Intl API.

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.date and i18n.number methods with direct use of Intl.DateTimeFormat and Intl.NumberFormat respectively.

  • 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:

import type { I18n } from "@lingui/core";

const dateOptions = { dateStyle: "medium" } as const;
const currencyOptions = {
  style: "currency",
  currency: "EUR",
} as const;

export function formatOrderSummary(i18n: I18n, date: Date, total: number) {
  const dateFormatter = new Intl.DateTimeFormat(i18n.locale, dateOptions);
  const numberFormatter = new Intl.NumberFormat(i18n.locale, currencyOptions);

  // "Jan 18, 2026 - €1,234.56"
  return `${dateFormatter.format(date)} - ${numberFormatter.format(total)}`;
}

In React you have to use useMemo to memoize formatters and react to locale changes:

import { useLingui } from "@lingui/react";
import { useMemo } from "react";

const dateOptions = { dateStyle: "medium" } as const;
const priceOptions = {
  style: "currency",
  currency: "USD",
  minimumFractionDigits: 2,
} as const;

function PriceLine({ date, total }: { date: Date; total: number }) {
  const { i18n } = useLingui();

  const dateFormatter = useMemo(
    () => new Intl.DateTimeFormat(i18n.locale, dateOptions),
    [i18n.locale]
  );
  const numberFormatter = useMemo(
    () => new Intl.NumberFormat(i18n.locale, priceOptions),
    [i18n.locale]
  );

  return (
    <span>
      {/* "Jan 18, 2026 - $1,234.56" */}
      {dateFormatter.format(date)} - {numberFormatter.format(total)}
    </span>
  );
}

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality not to work as expected)
  • Documentation update
  • Examples update

Fixes #2265

Checklist

  • I have read the CONTRIBUTING and CODE_OF_CONDUCT docs
  • I have added tests that prove my fix is effective or that my feature works
  • I have added the necessary documentation (if appropriate)

@vercel
Copy link

vercel bot commented Dec 16, 2025

@yslpn is attempting to deploy a commit to the Crowdin Team on Vercel.

A member of the Team first needs to authorize it.

@codecov
Copy link

codecov bot commented Dec 16, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 81.27%. Comparing base (6bb8983) to head (13c55fc).
⚠️ Report is 256 commits behind head on main.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@timofei-iatsenko
Copy link
Collaborator

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?

@yslpn
Copy link
Contributor Author

yslpn commented Dec 17, 2025

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 Intl method. I'd prefer to keep recommending native Intl.* directly. If someone needs helpers, they can create their own functions or hooks.

Does that sound good to you?

Blog post would be much apreciated to explain what was the driver for the decision.

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).

@vonovak vonovak self-requested a review December 19, 2025 22:16
@timofei-iatsenko
Copy link
Collaborator

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.

@yslpn
Copy link
Contributor Author

yslpn commented Jan 19, 2026

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.

@yslpn yslpn marked this pull request as draft January 19, 2026 11:34
Copy link
Collaborator

@vonovak vonovak left a comment

Choose a reason for hiding this comment

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

I added a few suggestions. We should revisit especiially the "Migrating to native Intl" section.
But thanks for the work!

Comment on lines +39 to +57
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)}`;
}
Copy link
Collaborator

@vonovak vonovak Jan 12, 2026

Choose a reason for hiding this comment

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

This suggested code has two problems:

  1. it assumes that i18n instance comes from @lingui/core. We should recommend code that is agnostic of the i18n instance origin. It could come from @lingui/core, from react context or other place. We should not assume.
  2. it's suboptimal because for each formatOrderSummary we create a new instance of the Intl objects. 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);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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);
Copy link
Collaborator

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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.
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should think about offering a codemod, or asking community to contribute a codemod.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It seems like it would be difficult to make any kind of reliable codemod here.

@andrii-bodnar
Copy link
Contributor

Hi @yslpn, thanks for the contribution!

I would suggest retargeting this PR to the next branch. Instead of a standalone blog post, we will include this information in the v6 migration guide and release notes. This type of change is a perfect fit for a major release, and keeping it bundled there helps avoid the overhead of a separate post.

@yslpn
Copy link
Contributor Author

yslpn commented Feb 7, 2026

I would suggest retargeting this PR to the next branch. Instead of a standalone blog post, we will include this information in the v6 migration guide and release notes. This type of change is a perfect fit for a major release, and keeping it bundled there helps avoid the overhead of a separate post.

@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?

@andrii-bodnar andrii-bodnar changed the base branch from main to next February 9, 2026 08:55
@andrii-bodnar
Copy link
Contributor

@yslpn yes, I will reuse its content in #2424 and #2427 (perhaps in more compact and compressed form)

@andrii-bodnar andrii-bodnar added this to the v6 milestone Feb 9, 2026
@yslpn yslpn marked this pull request as ready for review February 11, 2026 09:52
@yslpn
Copy link
Contributor Author

yslpn commented Feb 11, 2026

Hi @vonovak Can I ask you to review this again please? I made some changes. The most significant ones are:

  1. Deleted the blog post; @andrii-bodnar is now responsible for that for v6.

  2. I changed the examples.

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

  1. I didn't optimize the code too much to keep it simple for understanding the examples.
  2. Moved most of the text from the blog to the PR description.

@yslpn yslpn requested a review from vonovak February 11, 2026 10:54
@andrii-bodnar andrii-bodnar merged commit 70aa02c into lingui:next Feb 13, 2026
9 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Let's mark the wrappers over Intl methods as deprecated

4 participants