DEV Community

Cover image for Showing a Date Needs Three Decisions
bwi
bwi

Posted on

Showing a Date Needs Three Decisions

Why showing a date always involves three implicit decisions

Date representations feel trivial.
They are everywhere.
And because of that, they are rarely questioned.

MM/dd/yyyy
dd.MM.yyyy
dd. MMMM yyyy
Enter fullscreen mode Exit fullscreen mode

They look harmless.
They look technical.
They look solved.

But they aren't.

Behind every date representation are decisions — and most systems make them implicitly.


The core insight

A date representation always consists of three parameters. Always.

  • Format
  • Language
  • Region / cultural conventions

Even if we do not specify them explicitly.
Even if we rely on defaults.

Defaults do not remove these parameters.
They merely hide them.


1. Format — structure

The format defines how the date is structured:

dd.MM.yyyy
MM/dd/yyyy
dd. MMMM yyyy
Enter fullscreen mode Exit fullscreen mode

It controls:

  • order (day / month / year)
  • separators (., /, ,)
  • numeric vs textual representation

What it does not define:

  • language
  • region
  • cultural expectation

A format describes shape, not meaning.


2. Language — words

Language determines which words are used:

  • January / Januar / Jänner
  • Monday / Montag
  • March / März

But language alone says nothing about:

  • date order
  • separators
  • customary notation

"German" is not enough.
"English" is not enough.


3. Region / cultural conventions — expectations

The region defines what people expect to see:

  • dd.MM.yyyy vs MM/dd/yyyy
  • dots vs slashes
  • typical short and long forms
  • week start

Two users can:

  • speak the same language
  • read the same month name
  • and still expect different representations

These three parameters always exist

Even when we only specify one.

Example:

en
Enter fullscreen mode Exit fullscreen mode

What does this actually mean?

In most systems, it implicitly becomes something like:

Language: English
Region: US
Format: MM/dd/yyyy
Enter fullscreen mode Exit fullscreen mode

But these are assumptions, not facts.

No one explicitly chose them.
No one documented them.
No one can reliably explain them later.


Language + region as locale tags (IETF BCP 47)

So we have three parameters — but in practice, two of them (language and region) are usually expressed together as a locale tag.
A locale tag does not replace the format — it only covers language and region.
Most platforms follow a standard for this: IETF BCP 47.

So when we write de-AT or en-US, we are not just picking a language.
We are selecting a language and a regional set of conventions.

A (common) BCP 47 tag has the form:

language-region
Enter fullscreen mode Exit fullscreen mode
  • Language: ISO 639 (usually lowercase), e.g. de, en
  • Region: ISO 3166 (usually uppercase), e.g. AT, US, GB

The important part: language and region are independent dimensions.
Any combination can be meaningful:

  • en-AT — English language, Austrian conventions
  • de-US — German language, US conventions

Also note the order: AT is a region, so it comes second (en-AT), not first.

Some systems use an underscore notation like en_AT.
In the browser Intl APIs, the standard form is the hyphen: en-AT.

But even a full locale tag only covers two of the three decisions. The format is still a separate choice.


Defaults are not decisions

When we specify only part of the information, the system fills in the rest:

  • browser
  • operating system
  • runtime
  • library
  • framework

Each layer makes a reasonable guess.

Fallbacks do not replace missing parameters.
They only hide them.

Internally, there is still:

  • a concrete format
  • a concrete language
  • a concrete region

They are just implicit.


Example 1: dd. MMMM yyyy

Format: dd. MMMM yyyy
Enter fullscreen mode Exit fullscreen mode

This only means:

"Write the month name in full."

But which one?

  • January
  • Januar
  • Jänner

Without language and region, the format alone is meaningless.


Example 2: German is not just German

Language: German
Enter fullscreen mode Exit fullscreen mode

What does that imply?

  • de-DEJanuar
  • de-ATJänner

Both are correct.
Both are German.
And yet, they are not interchangeable.

Language alone is insufficient.


Example 3: English is not a culture

Locale: en-AT
Enter fullscreen mode Exit fullscreen mode

This means:

  • Language: English
  • Region: Austria

A perfectly valid and realistic scenario:

  • English UI
  • Austrian date conventions

English does not imply US culture.
It never did.


These parameters can come from different sources

In real systems, they rarely originate from one place.

Typical sources include:

  • Browser / OS — implicit, uncontrolled
  • User profile — explicit, but often incomplete
  • Admin / organization — standardized, intentional
  • Context / use case — UI, export, contract, audit log

Consider a real scenario:

A user's browser is set to de-DE.
Their organization is based in Austria.
An admin has configured dates to display in long format.

If the system resolves the locale from the browser, the user sees Januar. If it resolves from the organization, they see Jänner. Which one wins depends on the implementation — and that's rarely documented.

Either way, someone sees a date that doesn't match their expectation.

There is no single "correct" date.


Even the format is a decision

Most platforms offer predefined format levels:

short
medium
long
full
Enter fullscreen mode Exit fullscreen mode

But you can also define a fully custom format like dd. MMMM yyyy.

Either way, the choice of format is not a technical detail.
It is a product decision:

In which context do users see a short date? When do they see a long one?
Who decides — the developer, the admin, the product?

These are not implementation details. They belong to product design, configuration, and context — not just code.


The common mistake

We say:

"We support localization."

And what we actually mean is:

  • a format
  • a default locale
  • some Intl APIs

What we really have is:

a collection of implicit assumptions.

And those assumptions have consequences:

  • A contract rendered with 01/02/2025 — is that January 2nd or February 1st?
  • A user in Vienna sees Januar and wonders why the system doesn't know their country.
  • An audit log stores dates in the server's locale — which changes after a migration.

These are not edge cases.
They are the natural result of treating three decisions as one.


The key takeaway

A date is only reasonably represented when
format, language, and region are all explicitly defined.

Everything else is guessing.


Once you accept that these three decisions always exist, the remaining question is no longer if you should define them — but where.

What to do

  • Always pass format, language, and region explicitly — never rely on one to imply the others.
  • Treat format selection (short, long, full) as a product decision, not a developer default.
  • Define a clear priority for where each parameter comes from: user profile, tenant, admin policy, or context.
  • Never persist or export dates in a locale-dependent format without documenting which locale.
  • When in doubt, make the decision visible — in configuration, in documentation, in code.

In the browser: Intl.DateTimeFormat

The browser is one of the most common user interfaces — and it already has an API that models all three decisions explicitly: Intl.DateTimeFormat.

Format: dateStyle

The dateStyle option controls the shape of the output:

const date = new Date("2025-01-02");

new Intl.DateTimeFormat("de-AT", { dateStyle: "short" }).format(date)
// → "02.01.25"

new Intl.DateTimeFormat("de-AT", { dateStyle: "medium" }).format(date)
// → "02.01.2025"

new Intl.DateTimeFormat("de-AT", { dateStyle: "long" }).format(date)
// → "2. Jänner 2025"

new Intl.DateTimeFormat("de-AT", { dateStyle: "full" }).format(date)
// → "Donnerstag, 2. Jänner 2025"
Enter fullscreen mode Exit fullscreen mode

Same locale. Only the format changes.

Language + Region: the locale tag

The locale tag is where language and region are combined (BCP 47).
In Intl, use the hyphen form: de-AT (not de_AT).

const date = new Date("2025-01-02");

new Intl.DateTimeFormat("de-DE", { dateStyle: "long" }).format(date)
// → "2. Januar 2025"

new Intl.DateTimeFormat("de-AT", { dateStyle: "long" }).format(date)
// → "2. Jänner 2025"

new Intl.DateTimeFormat("en-US", { dateStyle: "long" }).format(date)
// → "January 2, 2025"

new Intl.DateTimeFormat("en-GB", { dateStyle: "long" }).format(date)
// → "2 January 2025"

new Intl.DateTimeFormat("en-AT", { dateStyle: "long" }).format(date)
// → "2 January 2025"
Enter fullscreen mode Exit fullscreen mode

Same date. Same format level. Completely different output — because language and region differ.

What happens when we leave things out?

new Intl.DateTimeFormat("en").format(new Date("2025-01-02"))
// → depends on the browser's region assumption
// Could be "1/2/2025" (US) or "02/01/2025" (GB)

new Intl.DateTimeFormat().format(new Date("2025-01-02"))
// → depends entirely on the browser/OS default locale
Enter fullscreen mode Exit fullscreen mode

This is exactly the problem: the API works — but only if we pass all three decisions explicitly.

When we leave the locale vague or omit dateStyle, the browser fills in the gaps. Silently. Differently on every machine.

The API already supports this

Intl.DateTimeFormat does not force us to guess. It lets us express all three decisions in a single call:

new Intl.DateTimeFormat("de-AT", { dateStyle: "long" })
Enter fullscreen mode Exit fullscreen mode
  • Format: long
  • Language: German
  • Region: Austria

Three parameters. One line. No ambiguity.

The tools exist. The question is whether we use them — or let the defaults decide for us.

Server-side runtimes follow the same model (for example: CultureInfo in .NET, Locale in Java) — but that's a topic for another post.


Conclusion

  • Every date representation requires three parameters
  • They exist even when we do not specify them
  • Defaults are assumptions, not decisions
  • Implicit decisions are the most dangerous ones

Localization does not start with APIs.
It starts where a product takes responsibility.

Top comments (0)