You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@groovy.apache.org by "Paul King (Jira)" <ji...@apache.org> on 2026/07/12 00:12:00 UTC
[jira] [Created] (GROOVY-12152) Clarify Locale handling throughout the GDK
Paul King created GROOVY-12152:
----------------------------------
Summary: Clarify Locale handling throughout the GDK
Key: GROOVY-12152
URL: https://issues.apache.org/jira/browse/GROOVY-12152
Project: Groovy
Issue Type: Improvement
Reporter: Paul King
h3. Summary
Assess and improve how the GDK handles {{java.util.Locale}}, following the discussion on the GROOVY-12147 dev thread. The goal is *not* to remove convenience methods, but to make each locale-sensitive method's contract explicit, offer an explicit-{{Locale}} variant where one is missing, and fix a couple of parse defaults before 6.0.0 ships. Almost all of this is additive/non-breaking.
h3. Background
Two recent efforts touched this area:
* GROOVY-12147 added locale-aware number/currency/percent formatting and parsing.
* GROOVY-12055 moved identifier/token case-folding to {{Locale.ROOT}}.
On the GROOVY-12147 thread it was noted that methods depending on the JVM {{default}} locale are hard to test and surprising on servers/multi-locale processes. The guiding principle we landed on:
* {{Locale.ROOT}} — data meant for machines: protocols, DB, file formats, anything persisted or re-parsed.
* {{Locale.getDefault()}} — output meant for the human running the code (only a valid proxy for "the user" in single-user/client contexts).
The work below applies that principle per method.
h3. Scope
All actionable items are in GDK extension classes ({{StringGroovyMethods}}, {{DefaultGroovyMethods}}, {{DateUtilExtensions}}/{{DateUtilStaticExtensions}}, {{DateTimeExtensions}}/{{DateTimeStaticExtensions}}).
h4. A. Case-insensitive / case-folding on human text (consistency + docs)
These take arbitrary user text, fold case, and are silent about their (locale-independent) contract.
|| Method || Current || Action ||
| {{containsIgnoreCase}} | {{toLowerCase(Locale.ROOT)}} both sides | Align mechanism with starts/ends; Javadoc "locale-independent" |
| {{startsWithIgnoreCase}}, {{endsWithIgnoreCase}} | JDK {{equalsIgnoreCase}} (char-wise) | Same doc note; pick one shared mechanism |
| {{capitalize}}, {{uncapitalize}} | {{Character.to{Upper,Lower}Case}} (single char) | Javadoc note only; low priority |
A {{Locale}} variant is generally *not* warranted here (search/compare wants stability).
h4. B. Parsing / machine-ingestion
|| Method || Current default || Action ||
| {{CharSequence.toCurrencyNumber()}}, {{toPercentNumber()}} (no-arg) | {{Locale.getDefault()}} | Change default to {{ROOT}} (or drop the no-arg) *before 6.0.0*; keep the {{(Locale)}} variant; doc |
| {{CharSequence.toNumber(Locale)}} | {{Locale}}-only, no no-arg — _correct as-is_ | Leave {{Locale}}-only. The locale-independent parse is already provided by {{toBigDecimal()}}/{{toInteger()}}/{{as Number}} (Java-literal grammar, not ROOT). A no-arg would either duplicate those or introduce divergent {{NumberFormat}} semantics ({{"1,234"}} parses via ROOT {{NumberFormat}} but throws via {{toBigDecimal}}). |
| {{Date.parse(format, input)}} / +TimeZone (static) | default locale ({{SimpleDateFormat}}) | Add {{(…, Locale)}} variant; doc |
| {{<java.time>.parse(text, pattern)}} ×9 types (static) | default locale ({{ofPattern}}) | Add {{(…, Locale)}} variant; doc |
h4. C. Human-facing formatting defaulting to the JVM default locale
Defensible defaults, but each needs (1) a Javadoc line "uses the JVM's locale; pass a {{Locale}} for server/per-user output" and (2) a {{Locale}} variant where none exists.
|| Method || Locale variant today? || Action ||
| {{Number.toCurrencyString()}} / {{toPercentString()}} (no-arg) | yes | Doc the default; currency no-arg is a drop candidate (ROOT is meaningless for currency) |
| {{Date.format(String)}}, {{format(String, TimeZone)}}, {{Calendar.format(String)}} | no | Add {{(…, Locale)}} variant + doc |
| {{Date.getDateString()}} / {{getTimeString()}} / {{getDateTimeString()}} | no | Add a locale-aware variant (or doc as fixed default-locale) |
| {{<java.time>.format(String pattern)}} ×6 | no | Add {{(…, Locale)}} variant + doc |
| {{<java.time>.format(FormatStyle)}} ×6 | no | Add {{(…, Locale)}} variant + doc |
| {{printf(...)}} ×4, {{sprintf(...)}} ×2 | no | Add a {{Locale}}-first overload (mirrors {{String.format(Locale, …)}}); doc |
h4. Cross-cutting: Javadoc {{@see}} routing
Wire bidirectional {{@see}} links so each method routes the reader to the right sibling: the locale-aware variant, the locale-free variant, and the raw JDK API ({{NumberFormat.getNumberInstance(Locale)}}, {{DateTimeFormatter.ofPattern(pattern, locale)}}, {{new SimpleDateFormat(pattern, locale)}}). This makes the "no no-arg is intentional" decisions self-documenting and keeps the JDK escape hatch one hop away.
h3. Out of scope
The GROOVY-12055 identifier/token case-folding (class/property/CLI/SQL names, etc., mostly in non-extension classes) stays on {{Locale.ROOT}} — the input domain is machine tokens, so ROOT is correct and no doc change is warranted.
h3. Note: existing precedent in groovy-json
{{groovy.json.JsonGenerator}} already models the pattern this issue recommends for machine-facing output:
{code:java}
protected static final String JSON_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ";
protected static final Locale JSON_DATE_FORMAT_LOCALE = Locale.US; // stable, not getDefault()
protected static final String DEFAULT_TIMEZONE = "GMT";
// ...
public Options dateFormat(String format, Locale locale) { ... } // explicit override
{code}
i.e. a stable default locale (not {{getDefault()}}), an explicit {{Locale}} override, and — being a config object — a *discoverable* default. Category C should converge on this shape.
h3. Acceptance / proposed policy
The rule is not "no default-locale methods" — it is "no method depends on the default locale _silently or without an escape hatch_". Concretely, a new locale-sensitive GDK method is acceptable when:
* It offers an explicit-{{Locale}} variant (or the non-locale form is documented as living elsewhere, e.g. {{toBigDecimal()}} for locale-free number parsing).
* Its no-arg/default behaviour is stated in the Javadoc — which locale it uses and why.
* The default is chosen by audience, not by habit:
** _Human-facing formatting_ (currency/percent/date-for-display, {{printf}}) *may* default to {{Locale.getDefault()}}, provided the {{Locale}} variant exists and the default is documented ("uses the JVM's locale; pass a {{Locale}} for server/per-user output").
** _Machine-facing / ingestion_ (parsing, serialization, anything persisted or re-parsed) defaults to {{Locale.ROOT}} or requires an explicit {{Locale}} — never {{getDefault()}}.
Additionally:
* Avoid new locale-sensitive method ships _default-locale-only_ (default behaviour with neither a {{Locale}} variant nor a documented contract), but if unavoidable, explicit Javadoc should make the default explicit and point to the JDK long-hand to get Locale behavior.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)