Skip to content
Back to skills

Wp Locale And Dates

ASecurity

Handle dates, times, and numbers in WordPress plugins with

  • 22 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added June 5, 2026
developmentgophpsqlapidatabase

Works with

  • api

Security analysis

A100/100

Scanned June 5, 2026

npx -y skills add Lonsdale201/wp-agent-skills --skill wp-locale-and-dates --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Wp Locale And Dates?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Wp Locale And Dates
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/lonsdale201-wp-locale-and-dates/badge)](https://www.skillsdirectory.com/skills/lonsdale201-wp-locale-and-dates)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: wp-locale-and-dates
description: Handle dates, times, and numbers in WordPress plugins with
  the modern (5.3+) locale-aware helpers — `wp_date()`,
  `current_datetime()`, `wp_timezone()`, `get_gmt_from_date()` /
  `get_date_from_gmt()`, `mysql_to_rfc3339()`, `number_format_i18n()` —
  and avoid the legacy foot-guns (`current_time('timestamp')` returning
  offset-summed not-quite-Unix, `date_i18n` quirks, `mysql_to_rfc3339`
  not actually being RFC3339). Covers `timezone_string` vs `gmt_offset`
  fallback, storing site-local + UTC MySQL columns, REST date emission,
  and locale-aware number formatting. Use for any plugin that stores,
  queries, or displays dates / numbers in multi-locale, multi-timezone
  installs.
author: Soczó Kristóf
contact: mailto:lonsdale201@hotmail.com
plugin: wordpress
plugin-version-tested: "5.3 - 7.0"
php-min: "7.4"
last-updated: "2026-05-24"
docs:
  - https://developer.wordpress.org/reference/functions/wp_date/
  - https://developer.wordpress.org/reference/functions/current_datetime/
  - https://developer.wordpress.org/reference/functions/wp_timezone/
  - https://make.wordpress.org/core/2019/09/23/date-time-improvements-wp-5-3/
---

# WordPress Locale, Dates & Numbers

WP's date/time API has TWO eras: pre-5.3 (offset-summed timestamps, `date_i18n`, `current_time('timestamp')` — quirky, error-prone) and 5.3+ (true UTC timestamps, `DateTimeZone` objects, `wp_date`, `current_datetime`). Most plugins still write pre-5.3 code because that's what AI training data is full of. This skill is the modern recipe.

## When to use this skill

Trigger when ANY of the following is true:

- A plugin stores, queries, or displays dates / times — scheduled tasks, expiry dates, audit timestamps, log entries, "X minutes ago" labels, REST date fields.
- Code references `wp_date`, `current_datetime`, `current_time`, `date_i18n`, `wp_timezone`, `wp_timezone_string`, `get_gmt_from_date`, `get_date_from_gmt`, `mysql2date`, `mysql_to_rfc3339`, `iso8601_to_datetime`, `number_format_i18n`.
- Code calls `time()`, `date()`, `strtotime()` directly for site-display dates without considering timezone.
- The user reports: "wrong timezone on schedule emails", "WP-Admin shows EST but database has UTC", "Friday in Europe shows as Thursday in REST".

## The mental model — timestamps are UTC, formatting is local

Since WP 5.3, the rule is:

- **Timestamps stored in code (and Unix epoch in general) are UTC integers**. `time()` returns UTC. `wp_date()` accepts UTC and formats in a target timezone.
- **MySQL DATETIME columns in WP are stored in the SITE's timezone** (`post_date` is local; `post_date_gmt` is UTC — that's why both columns exist).
- **The "site's timezone" is configurable** — `Settings → General → Timezone`. It's either `Europe/Budapest` (IANA name in `timezone_string`) or a manual `+02:00` offset (in `gmt_offset`). Use `wp_timezone()` / `wp_timezone_string()` — they handle the fallback for you (`wp-includes/functions.php:124-154`).

## Always reach for these (modern API)

| Need | Function | Notes |
|---|---|---|
| Current UTC timestamp | `time()` | Plain PHP. WP doesn't wrap. |
| Current local DateTimeImmutable | `current_datetime()` | Site timezone (5.3+) |
| Format a UTC timestamp for display | `wp_date( $format, $timestamp = null, $timezone = null )` | Default timezone = site. Default timestamp = now. Returns `false` on invalid input |
| Site timezone object | `wp_timezone()` | Returns `DateTimeZone`, never null |
| Site timezone string | `wp_timezone_string()` | IANA name OR `±HH:MM` offset |
| Convert site-local datetime → UTC | `get_gmt_from_date( $date_string, $format = 'Y-m-d H:i:s' )` | Takes a `Y-m-d H:i:s` string in site TZ, returns UTC. Validate input first |
| Convert UTC datetime → site-local | `get_date_from_gmt( $date_string, $format = 'Y-m-d H:i:s' )` | Mirror image. Validate input first |
| Locale-aware number | `number_format_i18n( $number, $decimals = 0 )` | Reads `$wp_locale->number_format['decimal_point' \| 'thousands_sep']` |

## Avoid these unless interfacing with legacy code

- **`current_time( 'timestamp' )`** — returns `time() + gmt_offset * 3600`. The result is **NOT a real Unix timestamp** — it's UTC summed with the site's offset. Pre-5.3 code uses it to feed `date()` directly. PHPCS flags it via `WordPress.DateTime.CurrentTimeTimestamp.Requested`. Use `time()` for UTC or `current_datetime()->getTimestamp()` for a proper local-now-as-UTC-int.
- **`date_i18n( $format, $timestamp_with_offset )`** — legacy wrapper around `wp_date`. Same trap as above: the `$timestamp` arg expects offset-summed values, not real UTC. `wp_date` is the cleaner replacement (`wp-includes/functions.php:179`).
- **`mysql_to_rfc3339( $date_string )`** — function name lies: source docblock explicitly says **"the output does NOT conform to RFC3339 format, which must contain timezone"** (`wp-includes/functions.php:7855`). It outputs `Y-m-d\TH:i:s` with no `Z` / no offset. Use it only if you specifically want WP's REST-compatible "local without offset" shape (that's the convention for REST `*_gmt` fields, despite the misleading name).

## Storing dates

### MySQL DATETIME columns

Standard WP convention: store both the site-local and the UTC version side by side. This is what `post_date` / `post_date_gmt` does and what most plugin tables should follow.

```php
$now_local = current_datetime();                          // DateTimeImmutable in site timezone
$now_utc   = $now_local->setTimezone( new DateTimeZone( 'UTC' ) );

$wpdb->insert( $wpdb->prefix . 'myplugin_events', array(
    'event_date'     => $now_local->format( 'Y-m-d H:i:s' ),
    'event_date_gmt' => $now_utc->format( 'Y-m-d H:i:s' ),
) );
```

If you only have a user-entered local date string (e.g. from a datepicker — see **`wp-admin-form-controls`**), convert at the boundary:

```php
$user_input = '2026-06-01 14:30:00';                      // assumed in site TZ
$gmt_string = get_gmt_from_date( $user_input );           // → 'Y-m-d H:i:s' in UTC
```

`get_gmt_from_date()` and `get_date_from_gmt()` are converters, not validators. On parse failure core returns `gmdate( $format, 0 )` (usually `1970-01-01 00:00:00`), so validate user input before conversion.

### Storage as Unix integer

For event timestamps, expiry, etc., store as a `BIGINT` UTC integer (`time()`). Simple to query (`WHERE expires_at < UNIX_TIMESTAMP()`), no timezone ambiguity. Display-side, feed to `wp_date()`.

```php
$row = array( 'expires_at' => time() + DAY_IN_SECONDS * 30 );
// ...
$display = wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $row['expires_at'] );
```

`get_option( 'date_format' )` and `get_option( 'time_format' )` are the user-configured formats from `Settings → General`. Use them so a German site shows `01.06.2026` and a US one shows `06/01/2026`.

## Display

```php
// Now, in site timezone, with site's date format.
echo esc_html( wp_date( get_option( 'date_format' ) ) );

// A stored UTC timestamp, shown in site timezone (default).
echo esc_html( wp_date( 'Y-m-d H:i', $expires_at_int ) );

// Same timestamp, but force UTC display.
echo esc_html( wp_date( 'Y-m-d H:i', $expires_at_int, new DateTimeZone( 'UTC' ) ) );

// Localized numbers.
echo esc_html( number_format_i18n( 1234567.89, 2 ) );    // de_DE: "1.234.567,89"
```

`wp_date` localizes day/month names via `$wp_locale`, but it does not rearrange your format string. Use the site's `date_format` / `time_format` options for normal UI instead of hardcoding English-style ordering.

## REST API dates

WP REST returns dates as `*_gmt` and `*` fields. The `_gmt` field uses the `mysql_to_rfc3339` shape (no `Z`/offset — confusing but it's the convention). Don't try to fight it; emit the same shape from your custom REST routes:

```php
// In a REST callback that builds a response:
$response_data = array(
    'created_at'     => mysql_to_rfc3339( get_date_from_gmt( $row['created_at_gmt'] ) ),
    'created_at_gmt' => mysql_to_rfc3339( $row['created_at_gmt'] ),
);
```

For genuine ISO 8601 with offset (needed if a third-party API expects RFC3339-compliant), use `DateTimeImmutable::format( DateTimeInterface::ATOM )` (= `'Y-m-d\TH:i:sP'`) directly.

## Locale-aware numbers

`number_format_i18n( $number, $decimals = 0 )` uses `$wp_locale->number_format['decimal_point']` and `['thousands_sep']` (verified at `wp-includes/functions.php:424`). It does NOT do currency formatting — that's WC's `wc_price()` territory. For non-WC currency display, build your own around `number_format_i18n` + a currency symbol option.

```php
$total = 1234.5;
$displayed = number_format_i18n( $total, 2 ) . ' ' . esc_html( get_option( 'myplugin_currency_symbol', 'USD' ) );
```

## Locale file loading

Translation loading is its own topic — see the existing **`wp-i18n-audit`** skill. Note for date/locale work specifically: `$wp_locale` is initialized as part of WP bootstrap; calling `wp_date` BEFORE the `init` action may produce English month names because the locale text domain hasn't loaded yet. Render dates from `init` or later.

## Critical rules

- **Treat `time()` results as UTC integers**. Pass them to `wp_date()` for display in site timezone, or `gmdate()` for UTC display. NEVER add the site offset to a `time()` value before formatting — that's the legacy `current_time('timestamp')` foot-gun.
- **`current_time( 'timestamp' )` is NOT a Unix timestamp**. It's `time() + offset * 3600`. PHPCS flags it. The correct replacement: `current_datetime()` for an object, `time()` for UTC int.
- **`mysql_to_rfc3339()` does NOT produce RFC3339**. The function name is wrong; it produces ISO 8601 without timezone. Source explicitly notes this. Use it for REST compatibility; use `DateTimeInterface::ATOM` for actually-RFC3339-compliant output.
- **If you store site-local MySQL DATETIME, also store the UTC companion**. Follow the `*_gmt` pattern (`event_date` + `event_date_gmt`) so display and UTC queries don't drift.
- **UTC-only storage is also valid for custom plugin tables**. A UTC `BIGINT` or UTC `DATETIME` can be simpler for expiry, logs, queues, and external integrations. Format at display with `wp_date()`.
- **`wp_timezone_string()` may return an IANA name OR a `±HH:MM` offset**. Don't assume IANA — sites with `gmt_offset` set but no `timezone_string` produce the offset form. `DateTimeZone()` accepts both, so use `wp_timezone()` (object) whenever possible.
- **Use `get_option('date_format')` + `get_option('time_format')` for user-facing display**, not hardcoded `'Y-m-d'`. Respects the site admin's preference.
- **Don't store locale-formatted strings**. Store machine values (`Y-m-d H:i:s` or UTC integers), then format at display. Locale-shaped strings are hard to query and migrate.
- **Don't render dates before `init`**. `$wp_locale` translation domain isn't bound yet; month/day names come back in English.

## Common AI mistakes

```php
// WRONG — uses pre-5.3 quirk; result is NOT a real Unix timestamp
$ts = current_time( 'timestamp' );
echo date( 'Y-m-d H:i', $ts );

// RIGHT — modern path
echo esc_html( wp_date( 'Y-m-d H:i' ) );
```

```php
// WRONG — date() in PHP uses server's PHP timezone (often UTC), not site's WP timezone
echo date( 'Y-m-d H:i', $created_at );

// RIGHT — wp_date respects site timezone setting
echo esc_html( wp_date( 'Y-m-d H:i', $created_at ) );
```

```php
// WRONG — adding offset to a UTC time twice; ends up double-shifted
$shifted = time() + (int) get_option( 'gmt_offset' ) * HOUR_IN_SECONDS;
echo date( 'H:i', $shifted );

// RIGHT — wp_date handles the timezone shift internally
echo esc_html( wp_date( 'H:i' ) );
```

```php
// WRONG — assumes IANA timezone; breaks when site uses gmt_offset
$tz = new DateTimeZone( get_option( 'timezone_string' ) );

// RIGHT — wp_timezone falls back to gmt_offset
$tz = wp_timezone();
```

```php
// WRONG — function name lies; produces ISO 8601 without TZ, NOT RFC3339
$rfc3339 = mysql_to_rfc3339( $datetime_string );  // for external API: WRONG

// RIGHT — actually-RFC3339
$rfc3339 = ( new DateTimeImmutable( $datetime_string, wp_timezone() ) )
    ->format( DateTimeInterface::ATOM );
```

## Cross-references

- See **`wp-i18n-audit`** for translation loading order — relevant because `$wp_locale->month` / `weekday` content depends on textdomain being loaded.
- See **`wp-admin-form-controls`** for the datepicker that emits `Y-m-d` strings, and the sanitize-callback strict validation that matches them.
- See **`wp-rest-api`** when emitting date fields in REST responses — match the `*_gmt` convention.

## What this skill does NOT cover

- Currency formatting. `number_format_i18n` formats numbers; currency display (locale-aware position of symbol, locale-specific separators) is its own topic. WC has `wc_price()`; non-WC plugins build a small wrapper.
- Custom calendar (Hijri, Buddhist, Jalali). WP only ships Gregorian. Use a third-party calendar library.
- Recurring schedules — see `wp-plugin-cron` + Action Scheduler skills.

## References

- `wp-includes/functions.php:78` — `current_time()`, including the PHPCS-flagged `'timestamp'` branch.
- `wp-includes/functions.php:101` — `current_datetime()` (5.3+, returns DateTimeImmutable).
- `wp-includes/functions.php:124` — `wp_timezone_string()` with `timezone_string` → `gmt_offset` fallback.
- `wp-includes/functions.php:152` — `wp_timezone()`.
- `wp-includes/functions.php:179` — `date_i18n()` (legacy wrapper around `wp_date`).
- `wp-includes/functions.php:243` — `wp_date()` (the modern entry point).
- `wp-includes/functions.php:424` — `number_format_i18n()`.
- `wp-includes/functions.php:7863` — `mysql_to_rfc3339()` with the "does NOT conform to RFC3339" docblock note.
- `wp-includes/formatting.php:3670` / `:3692` — `get_gmt_from_date()` / `get_date_from_gmt()`.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.