Repository navigation
fix(format date): render locales whose format carries %E or %O - #18918
Conversation
chrono expands %x, %X, %c and %r from the locale's own format strings at render time, so a modifier the user never typed can appear at that point. th_TH and lo_LA both carry %Ey (Buddhist era) in d_fmt, and th_TH also in d_t_fmt. chrono implements neither %E nor %O and reports a format error, so every format date under those locales failed -- including the one in default_env.nu, which greeted those users with a type mismatch on startup (nushell#15266). POSIX says an implementation with no alternative representation must behave as if the modifier were absent. Expand the locale specifiers here instead, with %E and %O stripped, and hand chrono a format it can render. Expanding rather than falling back to another locale keeps the locale's own field order: th_TH now reads 22/10/21, where a fallback to POSIX would have given 10/22/21. A locale can leave one of these strings empty -- de_DE, fr_FR and nl_NL have no am/pm form -- so an empty expansion is left alone for chrono to resolve, rather than blanking the output. Checked against chrono's own output across 14 locales x 5 formats: the only differences are the six that used to be errors.
|
Pushed upstream as you asked: chronotope/chrono#1816. It is one line in let spec = if spec == 'E' || spec == 'O' { next!() } else { spec };chrono's suite is green with it, and both new tests were mutation-checked — removing that line turns them red with the original panic: While building that I found two things wrong with what I told you here, so before anything else: 1. This PR does not fix all the affected locales. 2. The affected-locale list I gave you was wrong. I said The seven I never named are fixed by what merged — chrono has #18924 has that fix: substitute and green with the fix: If chrono#1816 lands and you pick up the release, Not verified: Linux and Rust stable only, |
…carries `%O` (#18924) Follow-up to #18918 (which fixed #15266). Two of the affected locales still fail on `main`, and the set of affected locales turned out to be larger than that PR said. Both found while working up the upstream chrono fix @fdncred asked for. # Description ## The bug #18918 left behind `az_IR` and `fa_IR` have an **empty** `t_fmt_ampm`, so #18918 deliberately leaves `%r` alone for chrono to resolve — that carve-out is what keeps `de_DE`/`fr_FR`/`nl_NL` from rendering blank. But chrono's fallback for an empty am/pm form is the locale's `t_fmt`, and for these two that is `%OH:%OM:%OS`. The modifier we just removed comes straight back in: ``` az_IR T_FMT_AMPM="" T_FMT="%OH:%OM:%OS" chrono-today=Err merged=Err proposed=Ok("20:00:12") fa_IR T_FMT_AMPM="" T_FMT="%OH:%OM:%OS" chrono-today=Err merged=Err proposed=Ok("20:00:12") de_DE T_FMT_AMPM="" T_FMT="%T" chrono-today=Ok("20:00:12") merged=Ok("20:00:12") proposed=Ok("20:00:12") fr_FR T_FMT_AMPM="" T_FMT="%T" chrono-today=Ok("20:00:12") merged=Ok("20:00:12") proposed=Ok("20:00:12") nl_NL T_FMT_AMPM="" T_FMT="%T" chrono-today=Ok("20:00:12") merged=Ok("20:00:12") proposed=Ok("20:00:12") ``` So `LC_TIME=fa_IR.UTF-8` still gets `nu::shell::type_mismatch, invalid format` from `date now | format date %r`. The fix is to make chrono's own fallback explicit: when `t_fmt_ampm` is empty, substitute `t_fmt` ourselves, so it goes through the stripping path like everything else. `de_DE`, `fr_FR` and `nl_NL` are byte-identical, since their `t_fmt` is `%T`. ## The affected locale list was wrong #18918 named `th_TH` and `lo_LA` and admitted the set was not enumerated exhaustively. It is nine, from scanning every `LC_TIME` block in `pure-rust-locales` 0.8.2: ``` az_IR: D_FMT=%Oy/%Om/%Od | D_T_FMT=%A %Oe %B %Oy، %OH:%OM:%OS | T_FMT=%OH:%OM:%OS fa_IR: D_FMT=%Oy/%Om/%Od | D_T_FMT=%A %Oe %B %Oy، %OH:%OM:%OS | T_FMT=%OH:%OM:%OS lo_LA: D_FMT=%d/%m/%Ey | D_T_FMT=%a %e %b %Ey, %H:%M:%S lzh_TW: D_FMT=%OC%Oy年%B%Od日 | D_T_FMT=%OC%Oy年%B%Od日 (%A) %OH時%OM分%OS秒 | T_FMT=%OH時%OM分%OS秒 | T_FMT_AMPM=%p %OI時%OM分%OS秒 mnw_MM: D_FMT=%OC%Oy %b %Od %A | D_T_FMT=%OC%Oy %b %Od %A %OI:%OM:%OS %Op %Z | T_FMT=%OI:%OM:%OS %p my_MM: D_FMT=%OC%Oy %b %Od %A | D_T_FMT=%OC%Oy %b %Od %A %OI:%OM:%OS %Op %Z | T_FMT=%OI:%OM:%OS %p or_IN: D_FMT=%Od-%Om-%Oy | D_T_FMT=%Oe %B %Oy %OI:%OM:%OS %p %Z | T_FMT=%OI:%OM:%OS %p shn_MM: D_FMT=%OC%Oy %b %Od %A | D_T_FMT=%OC%Oy %b %Od %A %OI:%OM:%OS %Op %Z | T_FMT=%OH:%OM:%OS %p th_TH: D_FMT=%d/%m/%Ey | D_T_FMT=%a %e %b %Ey, %H:%M:%S ``` The good news is that the existing approach already covers the seven that were never named — chrono has `%C`, `%e`, `%I`, `%p` and the rest, so stripping is enough. `lzh_TW` `%c` renders `2021年十月22日 (週五) 20時00分12秒`, `my_MM` `%X` renders `08:00:12 ညနေ`. Only the two `%r` cases above needed code. A test now pins all nine so the next locale table bump cannot quietly break one. # User-Facing Changes `format date` with `%r` under `LC_TIME=az_IR` or `fa_IR` renders instead of erroring. Nothing else changes: every other locale, including the empty-am/pm ones, produces the same bytes as before. # Tests + Formatting - `resolves_the_am_pm_fallback_when_it_also_carries_a_modifier` — the new case, `az_IR`/`fa_IR`. - `falls_back_to_the_plain_time_format_when_the_locale_has_no_am_pm` — the old `de_DE`/`fr_FR` test, updated for the new resolution and still asserting the rendered output is unchanged. - `renders_every_locale_that_carries_an_alternative_modifier` — `%x`, `%X`, `%c`, `%r` under all nine. Mutation-checked rather than trusted green. Dropping the fallback turns three tests red with the reported error: ``` test strings::format::date::test::resolves_the_am_pm_fallback_when_it_also_carries_a_modifier ... FAILED test strings::format::date::test::falls_back_to_the_plain_time_format_when_the_locale_has_no_am_pm ... FAILED test strings::format::date::test::renders_every_locale_that_carries_an_alternative_modifier ... FAILED az_IR %r did not render: Error(TypeMismatch { err_message: "invalid format", span: Span(TEST) }) ``` Green: ``` $ cargo test -p nu-command --lib test result: ok. 814 passed; 0 failed; 2 ignored; 0 filtered out; across 4 groups $ cargo test -p nu-command --test tests test result: ok. 2930 passed; 0 failed; 35 ignored; 0 filtered out; across 29 groups ``` `cargo fmt --all -- --check` and `cargo clippy -p nu-command --all-targets` are clean. Linux, Rust stable, `nu-command` only. I did not run the full workspace suite or the Windows/macOS paths. # After Submitting The upstream fix @fdncred asked for is chronotope/chrono#1816 — one line in chrono's `parse_next_item` that skips `%E`/`%O` per POSIX, with the same nine locales tested. If that lands and nushell picks up the release, this whole `resolve_locale_specifiers` layer and the `pure-rust-locales` direct dependency can be deleted. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: aron-intframe <[email protected]> Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
|
I just noticed that you didn't follow the PR template. Please update to follow it. We use the information for making release notes. |
|
You're right, and it isn't close: the template was rewritten four months before I opened this PR, so I had no excuse. The body here had I did the same to the follow-up #18924, which you also labelled One thing I hit while checking, which is not about my PRs. The generator in nu_scripts, Across the last 120 PRs merged into So if that script is still what builds the notes, 94 of those fall into the Not verified: I could not execute |
|
The response and all the description and user facing changes text is way too verbose. You should tell you LLM to be more concise. Nobody writes like that. |
|
You're right, and the gap is measurable. I counted words in the bodies of the last 168 PRs merged into Both sat past the 90th percentile, and CONTRIBUTING.md asks for the release notes section to be "brief and simple to understand", with technical detail moved out of it. I've rewritten both bodies in place, same three template sections, no code touched: The user-facing section is now one short paragraph and one before/after example per PR. The locale tables, the mutation-test transcripts and the per-locale output dumps are deleted, not relocated. Same rule from here on, including in comments like this one. |
|
Thanks |
Description
format dateerrored under any locale whose ownLC_TIMEstrings carry the POSIX modifiers%E/%O, such asth_TH's%Ey(Buddhist era). chrono expands%x,%X,%cand%rfrom those strings but implements neither modifier, sodate now | format date '%x %X'fromdefault_env.nufailed at every startup for those users.Those four specifiers are now expanded here from the locale's own tables with
%E/%Ostripped, which is what POSIX says to do when there is no alternative representation. Expanding rather than falling back to another locale keeps the locale's field order (22/10/21, not10/22/21); only the era is lost, and chrono cannot render it anyway. An emptyt_fmt_ampm(de_DE,fr_FR,nl_NL) is left for chrono so%rdoes not go blank.This makes
pure-rust-localesa direct dependency ofnu-command. chrono already pulls it in forunstable-locales, soCargo.lockis+1 −0with no new package.6 new unit tests, mutation-checked.
cargo test -p nu-command --test tests: 2917 passed. fmt and clippy clean. Linux andnu-commandonly.User-facing changes (Release notes)
format dateno longer errors under Thai, Lao and similar localesformat datewith%x,%Xor%cfailed withnu::shell::type_mismatch, invalid formatunder locales such asth_THandlo_LA, which write the year with a modifier the date library does not implement. The default config runsdate now | format date '%x %X', so affected users saw it at every startup. These now render in the locale's own field order, without the era:A modifier you type yourself no longer errors either:
%Eyand%Odnow render as%yand%dwould.Additional notes
Fixes #15266.
%Eand%Oinstead of failing to format chronotope/chrono#1816. If it lands and Nushell picks up the release,resolve_locale_specifiersand the direct dependency can both be deleted.%rwhen the locale's am/pm fallback also carries%O#18924 coversaz_IRandfa_IR, which this PR misses.