DateTimeFormatterJava 8 introduced a modern date and time API with new types like LocalDate available in java.time.* package. It also provided a new DateTimeFormatter class for formatting (generating strings from) those dates and times.
To create an instance of a DateTimeFormatter, one should use its static methods/fields or use the underlying DateTimeFormatterBuilder class like below examples:
DateTimeFormatter.ofPattern("yyyy-MM-dd")
// or
DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL)
// or
DateTimeFormatter.ISO_LOCAL_DATE_TIME
// or
new DateTimeFormatterBuilder().appendValue(ChronoField.YEAR).toFormatter()
They all return an immutable DateTimeFormatter instance.
Now, it is possible to override/change some aspects of the formatter by calling the builder-like .with***() methods or similar on the DateTimeFormatter instance which create a new copy instance with that aspect changed:
DateTimeFormatter
.ofPattern("yyyy-MM-dd")
.withChronology(JapaneseChronology.INSTANCE) // New instance with this chronology
.withDecimalStyle(DecimalStyle.of(Locale.of("fa"))) // New instance with this decimal style
But, localization (at least the numeral system) is not correct for these cases:
Locale.setDefault(myLocale); and then creating a formatter instance and using itofPattern("...", myLocale) method of DateTimeFormattertoFormatter(myLocale) method of DateTimeFormatterBuilderLocale.forLanguageTag("en-u-nu-arabext")), and using the resulting locale in withLocale(myLocale)As for Java legacy DateFormat, ICU4J, and JavaScript explained below, all the above cases should produce proper digits according to the locale. But Java DateTimeFormatter requires an additional redundant localizedBy(myLocale) or withDecimalStyle(DecimalStyle.of(myLocale)) call. Many/most people and even advanced programmers use the locale in ofPattern() method or in withLocale() instead of using localizedBy() and none of those correctly localize things like decimal style (digits/numbers). In my opinion, this is definitely a bug and not a feature:
Unlike withLocale(), the localizedBy() is not even mentioned in the top-level javadoc of DateTimeFormatter class or, for example, javadoc of ofPattern method.
The legacy Java DateFormat and SimpleDateFormat classes properly localize digits:
var myLocale = Locale.forLanguageTag("fa");
var formatter = new SimpleDateFormat("yyyy-MM-dd", myLocale);
var myDate = formatter.parse("2025-06-28");
formatter.format(myDate); // ۲۰۲۵-۰۶-۲۸
ICU4J also correctly formats numbers for both legacy and modern Java types:
var myLocale = Locale.forLanguageTag("fa");
var formatter = com.ibm.icu.text.SimpleDateFormat("yyyy-MM-dd", myLocale);
var myDate = LocalDate.of(2025, 6, 28);
formatter.format(myDate); // ۱۴۰۴-۰۴-۰۷
As another example, JavaScript also properly localizes everything (including numerals) when formatting the dates with its toLocaleString() or toLocaleDateString() methods:
let myDate = new Date(2025, 5, 28);
myDate.toLocaleDateString("fa"); // ۱۴۰۴/۴/۷
withLocale() and localizedBy()?localizedBy() produces a more thoroughly localized result, is there a reasonable use‑case for calling withLocale() instead?I agree with you that one would expect localization to include localized digits.
I believe the documentation is pretty clear on most points:
withLocale() provides a locale used for texts and format patterns. Text is the names and abbreviations of months and days of the week and more used in different languages. Patterns is for printing and parsing 8/23/2025 in the US and 23/08/2025 in France. As documented below, the method does control the week scheme used with the Gregorian calendar, including on which day of the week a new week begins.localizedBy() provides a locale for the same and more, like decimal style (also documented below). The documentation promises to control calendar system (what Java calls the chronology; for example use of the Hijri calendar) and time zone, which I haven’t tried.So the difference is that localizedBy() also localizes decimal style, chronology and time zone, which withLocale() does not. If decimal style, chronology and/or time zone is given in the locale, that is, which decimal style usually is and the others usually are not.
There may be situations where one wants the former without the latter, not that I know of any. For the purposes I can readily think of, I see no reason why not to take the “full package” promised by the latter method. The result of localizedBy() certainly is more thoroughly localized, which one (depending on needs and/or taste) may take to be more reliable.
As the documentation also says, withLocale() was introduced in Java 8 along with java.time and DateTimeFormatter. localizedBy() was added in Java 10.
A not too friendly interpretation would be that it was a bug that withLocale() did not include localized digits and time zone. When they came around to Java 10, they (rightfully IMHO) dared not fix the bug because it would break code relying on the behaviour in Java 8 and 9. Instead they added a new method with the functionality that the original method should have had.
Why withLocale() localizes some aspects and not others we don’t know. One may speculate that it could have been because of oversight or because of lack of time to complete it before it was released. As mentioned, it does localize the formats used by DateTimeFormatter.ofLocalized[Date][Time]() and the words and abbreviations used for months, days of month, AM/PM, eras, etc. Within locales that use Western (so-called Arabic) digits the method served a good purpose in Java 8 and 9. There certainly is one point in keeping the method in Java 10+: backward compatibility; not breaking code that uses it. I don’t know of any other point.
The fact that withLocale() does not localize numerals/digits has been reported as a bug, see the link at the bottom. The bug has been closed as “not an issue”, though. The bug report tells us:
This came up before, and we added
localizedBy(locale)method in JDK 10. ie. the correct solution for full localization should be:myDateTimeFormatter.localizedBy(userLocale);
You probably tried some things out yourself. I do too so we all know what we are talking about and to illustrate what the documentation already tells us.
Locale farsi = Locale.of("fa", "IR");
Locale french = Locale.of("fr", "FR");
LocalDate date = LocalDate.of(2025, Month.AUGUST, 23);
DateTimeFormatter builtIn = DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG);
System.out.println(date.format(builtIn.withLocale(farsi)));
System.out.println(date.format(builtIn.localizedBy(farsi)));
System.out.println(date.format(builtIn.withLocale(french)));
System.out.println(date.format(builtIn.localizedBy(french)));
Output:
23 اوت 2025
۲۳ اوت ۲۰۲۵
23 août 2025
23 août 2025
This confirms what you already said: withLocale() sets the date format and the names of months but does not set the digits used. localizedBy() does all of it.
Trying the same with a custom-built formatter can give us a bit more of information:
DateTimeFormatter custom = DateTimeFormatter.ofPattern("e EEEE d MMMM u");
System.out.println(date.format(custom.withLocale(farsi)));
System.out.println(date.format(custom.localizedBy(farsi)));
System.out.println(date.format(custom.withLocale(french)));
System.out.println(date.format(custom.localizedBy(french)));
1 شنبه 23 اوت 2025
۱ شنبه ۲۳ اوت ۲۰۲۵
6 samedi 23 août 2025
6 samedi 23 août 2025
One thing we observe here is that both withLocale() and localizedBy() cause the formatter to use the localized week start for the number of the day in the week. Lowercase e in the format pattern stirng gives us the number of the day in the week from 1 through 7. In Iran the week begins on Saturday, so using the Farsi locale 1 is printed for the day number. In France the week begins on Monday, so Saturday is the 6th day of the week. I do not find it clear from the documentation that also withLocale() enforces localized numbers of the days of the week; but it does.
DateTimeFormatter formatted numbers are not localized in the JDK bug system.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With