Formatting a date in Lua looks like a one-liner until the output has to read correctly in Japanese, Arabic, German, and Brazilian Portuguese — all from the same code path. That’s the moment Unicode’s date pattern system stops being trivia and starts being infrastructure. Unicode doesn’t just define characters; it ships a massive body of locale data that tells you how each culture orders a date, which calendar it uses, whether it wants midnight written as 24:00 or 00:00, and even which digits to render. Lua, being deliberately small, gives you almost none of that out of the box. The gap between the two is where the real work lives.
Below, the following sections cover everything from the anatomy of a Unicode date pattern to the UTF-8 traps that quietly corrupt output, so you can pick the right approach and ship dates that actually look local:
- Why Unicode formatting differs from classic date printing
- Pattern letters and what their widths control
- Skeletons and automatic locale-aware ordering
- Bridging Unicode data into a Lua project
- Time zones, non-Gregorian calendars, and native numerals
- Performance habits and testing checklists
Why Unicode Formatting Isn’t Just Pretty Printing
Classic date printing gives you a fixed template: you declare the order, the separators, the abbreviations, and every locale gets the same shape. Unicode’s approach inverts that. You describe what you want — year, month, day, weekday — and the locale data decides how it appears.
That distinction matters because date conventions aren’t cosmetic. In some locales the day comes first, in others the month, in others the year. Some languages inflect month names depending on grammatical case. Some regions use a calendar that isn’t the Gregorian one. Some prefer native digit shapes over ASCII numerals. A hardcoded format string can’t survive that, but a Unicode pattern plus locale data can.
The Anatomy of a Unicode Date Pattern
A Unicode date pattern is a string of field letters, each of which can be repeated to signal a different width. Width is not decoration — it changes the output.
Field letters worth memorizing
- y — year.
yis minimal digits,yyis two-digit,yyyyis zero-padded to four. - M / L — month.
Mfor formatting context,Lfor standalone context such as a month picker header. - d — day of month,
D— day of year,F— weekday-in-month. - E / e / c — weekday name or number, with local and ISO-style numbering variants.
- a / b / B — day periods.
Benables flexible periods like “in the morning” or “at night” when the locale supports them. - h / H / K / k — four different hour conventions, including 1–24 and 0–23.
- m, s, S — minute, second, and fractional seconds.
- z, O, v, V, X, x — time zone names and GMT offsets in several flavors.
Width changes the output
Width follows a predictable ladder. One letter gives a numeric value with no padding. Two letters gives numeric with padding. Three letters gives an abbreviated text form, four gives the full text form, and five gives the narrow form. So a month of September can render as 9, 09, Sep, September, or S from the same field letter. Day and month names come straight from locale data, which is why they appear correctly translated without you writing a single translation table.
Skeletons: Let the Locale Choose the Order
If patterns are too rigid, skeletons are the fix. A skeleton is a stripped-down request like “year, abbreviated month, day” expressed in any order. The formatting engine matches it against the locale’s preferred patterns and returns the right arrangement automatically.
// request: yMMMd
// en-US resolves to: MMM d, y
// de-DE resolves to: d. MMM y
// ja-JP resolves to: y年M月d日
This is the single highest-leverage idea in Unicode date formatting. You stop guessing and let curated locale data do the ordering. Skeletons also handle the ambiguous middle ground — like whether a locale wants a comma before the year — that hand-written patterns always get subtly wrong.
Making Unicode Patterns Work in Lua
Lua’s standard library exposes the operating system’s C-style time formatting. That’s fast and dependency-free, but it has no concept of locale-aware month names, skeletons, or Unicode pattern letters. To use Unicode patterns you need a library that either binds to native internationalization data or ships a compact table of pattern data itself.
The UTF-8 trap
This is where most Lua date code breaks. Lua strings are byte sequences. A localized month name like a Japanese or Cyrillic string is multi-byte, so #str returns the byte count, not the character count, and string.sub can slice a character in half and produce garbage. Use the built-in UTF-8 helpers for offsets and iteration, and never truncate a formatted date with byte arithmetic. If you need to clip a long weekday name for a narrow UI slot, do it at the character boundary or, better, request the abbreviated width instead.
Quoting literal text
Any character that isn’t a field letter needs quoting if it could be confused for one — typically with single quotes. Getting this wrong is a classic bug: a stray letter in a separator gets interpreted as a field and silently produces nonsense. When in doubt, quote.
Caching matters
Resolving locale data is not free. Build the formatter once per locale and reuse it, rather than constructing a new one inside a rendering loop. In a log viewer or a dashboard refreshing every second, that single change can dominate your performance profile.
Time Zones, Calendars, and Numerals
Locale awareness extends well beyond month names.
- Time zones. Offset patterns render signs and separators in locale-appropriate ways, and some offset styles omit the colon or use a different prefix. Pick the pattern deliberately instead of assuming a format.
- Calendars. Unicode data covers non-Gregorian systems, including lunar and solar regional calendars. Formatting a timestamp with the wrong calendar produces a plausible-looking but incorrect date.
- Numerals. Many locales expect native digit shapes by default. If your downstream parser assumes ASCII digits, that’s a breakage waiting to happen — decide explicitly whether you want localized or Latin numerals.
- Day periods. Some cultures split the day into more than two halves. Flexible day period patterns respect that; a binary AM/PM switch does not.
Common Mistakes to Avoid
- Hardcoding a comma between the day and year for every locale.
- Storing timestamps in local time instead of UTC and converting only at display.
- Using byte length to measure or trim formatted output.
- Treating
MandLas interchangeable when the month appears without a day. - Assuming two-digit years and 12-hour clocks are universal defaults.
- Ignoring daylight saving transitions when computing local output.
A Testing Checklist That Actually Catches Bugs
Run your formatter against a deliberately awkward set of locales: one with a non-Latin script, one with right-to-left text, one using a non-Gregorian calendar, and one with native numerals. Add a timestamp near midnight, one near a year boundary, and one during a daylight saving switch. Then parse the formatted output back into a date — round-tripping exposes assumptions that visual inspection misses. If a test locale’s output looks wrong to you but matches the locale data, trust the data.
Dates That Feel Native, Code That Stays Sane
Unicode date formatting rewards a small shift in mindset: describe the fields you want, then let locale data handle the presentation. In Lua that means leaning on the UTF-8 helpers, caching formatters, keeping UTC internally, and choosing skeletons over hand-built patterns whenever ordering might vary. Get those four habits right and your dates will read naturally in every language you support — without a pile of conditionals.
There’s more where this came from. Whether you’re untangling encoding issues, tuning app performance, or keeping up with the next wave of tooling, TechBlazing is built to give you the straight answer fast. Dive into the rest of the site and find your next useful thing.