Skip to content
Java i18n 6 min read

Formatting Dates & Times

Displaying a date as “06/13/2026” works fine for a US audience — but a German user expects “13.06.2026”, a Japanese user expects “2026年6月13日”, and an ISO-standard log file wants “2026-06-13”. Java gives you the tools to handle all of these without writing a single if (locale == ...) branch.

Note: Java has two date/time APIs: the modern java.time package (introduced in Java 8) and the legacy java.util.Date/java.text.DateFormat API. You should use java.time for all new code. This page covers both, so you can maintain older code confidently.

The Modern Way — DateTimeFormatter

java.time.format.DateTimeFormatter is the go-to class for locale-aware date and time formatting in modern Java. It is immutable and thread-safe, so you can safely declare a formatter as a static final field and reuse it across threads.

Predefined FormatStyle Patterns

The easiest approach is to use one of the four FormatStyle values: FULL, LONG, MEDIUM, and SHORT. Java derives the exact pattern from the target locale automatically — no manual pattern string required.

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Locale;

public class FormatStyleDemo {
    public static void main(String[] args) {
        LocalDate date = LocalDate.of(2026, 6, 13);

        Locale[] locales = { Locale.US, Locale.GERMANY, Locale.JAPAN, Locale.FRANCE };

        for (Locale locale : locales) {
            DateTimeFormatter fmt = DateTimeFormatter
                    .ofLocalizedDate(FormatStyle.LONG)
                    .withLocale(locale);
            System.out.printf("%-30s %s%n", locale.getDisplayName(), date.format(fmt));
        }
    }
}

Output:

English (United States)        June 13, 2026
German (Germany)               13. Juni 2026
Japanese (Japan)               2026年6月13日
French (France)                13 juin 2026

DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG) creates a formatter that knows how each locale orders day, month, and year. You never hard-code a pattern like "MMMM dd, yyyy".

FormatStyle Comparison

StyleUS English ExampleGerman Example
FULLSaturday, June 13, 2026Samstag, 13. Juni 2026
LONGJune 13, 202613. Juni 2026
MEDIUMJun 13, 202613.06.2026
SHORT6/13/2613.06.26

Tip: SHORT is compact for UI tables; FULL is great for formal documents. MEDIUM is usually the best default for user-facing dates when in doubt.

Formatting Date and Time Together

Use ofLocalizedDateTime(dateStyle, timeStyle) to format both parts at once:

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Locale;

public class DateTimeStyleDemo {
    public static void main(String[] args) {
        LocalDateTime now = LocalDateTime.of(2026, 6, 13, 14, 30, 0);

        DateTimeFormatter fmt = DateTimeFormatter
                .ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT)
                .withLocale(Locale.US);

        System.out.println(fmt.format(now));
    }
}

Output:

June 13, 2026 at 2:30 PM

Custom Pattern Strings with a Locale

Sometimes you need a specific layout that no FormatStyle provides — for example, a database column header or an API payload. Use DateTimeFormatter.ofPattern(String, Locale) and always pass the locale explicitly:

import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Locale;

public class CustomPatternDemo {
    public static void main(String[] args) {
        ZonedDateTime zdt = ZonedDateTime.of(2026, 6, 13, 14, 30, 0, 0,
                ZoneId.of("America/New_York"));

        // Month name will be in French because we pass Locale.FRANCE
        DateTimeFormatter fmt = DateTimeFormatter.ofPattern("d MMMM yyyy, HH:mm z", Locale.FRANCE);
        System.out.println(fmt.format(zdt));
    }
}

Output:

13 juin 2026, 14:30 EDT

Warning: If you call DateTimeFormatter.ofPattern("d MMMM yyyy") without a locale, the JVM uses the platform default locale. This produces unpredictable output in multi-tenant or cloud environments. Always call .withLocale(locale) or use the two-argument ofPattern(pattern, locale).

Parsing Locale-Specific Date Strings

Parsing works in reverse: the formatter reads a locale-specific string and returns a date/time object.

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Locale;

public class ParseDemo {
    public static void main(String[] args) {
        String germanDate = "13. Juni 2026";

        DateTimeFormatter fmt = DateTimeFormatter
                .ofLocalizedDate(FormatStyle.LONG)
                .withLocale(Locale.GERMANY);

        LocalDate date = LocalDate.parse(germanDate, fmt);
        System.out.println(date); // ISO-8601 output: 2026-06-13
    }
}

Output:

2026-06-13

Time Zones and ZonedDateTime

When you need to display times across time zones, use ZonedDateTime paired with a formatter. The z or V pattern letter prints the zone name or ID in the target locale’s language.

import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Locale;

public class ZonedDemo {
    public static void main(String[] args) {
        ZonedDateTime meeting = ZonedDateTime.of(2026, 6, 13, 9, 0, 0, 0,
                ZoneId.of("Asia/Tokyo"));

        DateTimeFormatter fmt = DateTimeFormatter
                .ofLocalizedDateTime(FormatStyle.FULL)
                .withLocale(Locale.JAPAN);

        System.out.println(fmt.format(meeting));
    }
}

Output:

2026年6月13日土曜日 9時00分00秒 日本標準時

The Legacy API — DateFormat and SimpleDateFormat

Before Java 8, java.text.DateFormat and its subclass SimpleDateFormat were the standard tools. You will encounter them in older codebases.

import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;

public class LegacyFormatDemo {
    public static void main(String[] args) {
        Date now = new Date(); // legacy java.util.Date

        DateFormat usFormat = DateFormat.getDateInstance(DateFormat.LONG, Locale.US);
        DateFormat frFormat = DateFormat.getDateInstance(DateFormat.LONG, Locale.FRANCE);

        System.out.println(usFormat.format(now));
        System.out.println(frFormat.format(now));
    }
}

Output:

June 13, 2026
13 juin 2026

The DateFormat.getDateInstance(style, locale) static factory mirrors DateTimeFormatter.ofLocalizedDate(FormatStyle) conceptually, but operates on java.util.Date objects.

Warning: SimpleDateFormat is not thread-safe. Never share a SimpleDateFormat instance between threads without synchronization. If you need a legacy pattern formatter in a concurrent context, create a new instance per thread (or better, migrate to DateTimeFormatter).

Legacy Style Constants

DateFormat ConstantEquivalent FormatStyle
DateFormat.FULLFormatStyle.FULL
DateFormat.LONGFormatStyle.LONG
DateFormat.MEDIUMFormatStyle.MEDIUM
DateFormat.SHORTFormatStyle.SHORT

Combining ResourceBundle with Date Formatting

In a real i18n application, you typically load a message template from a ResourceBundle and then inject a formatted date into it using MessageFormat:

import java.text.MessageFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Locale;

public class BundleDateDemo {
    public static void main(String[] args) {
        Locale locale = Locale.GERMANY;
        LocalDate expiry = LocalDate.of(2026, 12, 31);

        // In a real app, this pattern comes from a ResourceBundle
        String pattern = "Ihr Abonnement läuft am {0} ab.";

        DateTimeFormatter fmt = DateTimeFormatter
                .ofLocalizedDate(FormatStyle.LONG)
                .withLocale(locale);
        String formattedDate = expiry.format(fmt);

        System.out.println(MessageFormat.format(pattern, formattedDate));
    }
}

Output:

Ihr Abonnement läuft am 31. Dezember 2026 ab.

Under the Hood

How DateTimeFormatter Resolves Locale Patterns

When you call DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG).withLocale(Locale.GERMANY), the JVM does not use a hard-coded format string for German. Instead it delegates to java.text.spi.DateFormatProvider implementations — by default backed by Unicode CLDR (Common Locale Data Repository) data bundled into the JDK. CLDR is the world’s largest locale database, maintained by the Unicode Consortium, and it drives the locale-specific patterns for over 400 locales.

From Java 9 onward, the JDK ships with CLDR data as the default provider (java.locale.providers=CLDR is the default). Earlier JDK versions defaulted to the JRE’s own (less comprehensive) data, which is why you may see slightly different output between Java 8 and Java 11+ for the same locale.

Thread Safety

DateTimeFormatter is immutable. Once created, you can store it in a static final field and use it from any number of threads simultaneously with no synchronization overhead.

SimpleDateFormat, by contrast, stores intermediate parsing/formatting state as mutable fields on the instance itself, making it inherently thread-unsafe. A common bug in legacy code is declaring a static SimpleDateFormat for performance and then experiencing garbled dates under load.

ISO-8601 as a Safe Interchange Format

When storing dates in databases, sending them over APIs, or writing log files, always use ISO-8601: 2026-06-13T14:30:00Z. DateTimeFormatter.ISO_OFFSET_DATE_TIME and DateTimeFormatter.ISO_LOCAL_DATE are predefined formatters for this. ISO dates are locale-independent and sort correctly as strings — two properties that locale-formatted strings do not have.

import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;

public class IsoDemo {
    public static void main(String[] args) {
        OffsetDateTime odt = OffsetDateTime.of(2026, 6, 13, 14, 30, 0, 0, ZoneOffset.UTC);
        System.out.println(odt.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
    }
}

Output:

2026-06-13T14:30:00Z

Tip: Use locale-aware formatting only at the presentation layer (what the user sees). Everywhere else — persistence, APIs, logging — use ISO-8601. This makes your data portable and unambiguous.

  • Date/Time API (java.time) — Deep dive into LocalDate, LocalDateTime, ZonedDateTime, and the rest of the modern date/time library.
  • Internationalization — The broader i18n picture: Locale, ResourceBundle, MessageFormat, and collation.
  • ResourceBundle — Load locale-specific message templates that you can inject formatted dates and numbers into.
  • Formatting Numbers & Currency — Companion page: format integers, decimals, percentages, and monetary amounts for any locale.
  • String Methods — Methods like String.format() and locale-sensitive toLowerCase(Locale) complement date formatting in output construction.
Last updated June 13, 2026
Was this helpful?