Upgrade from Humanizer 3.0.10 to Humanizer 4
Use this guide to move an application, library, build tool, or custom Humanizer
extension from 3.0.10 to Humanizer 4. It covers migration work: package
replacement, source breaks, and intentional output changes. New opt-in features
belong in What's New, and the complete change history belongs in the
GitHub releases.
This guide was audited against commit
42b876dd85a882e3ebee377d36be388b2fbb0b34.
Humanizer 4 does not yet have a final tag. Replace <humanizer-4-version> below
with the candidate you are testing, and repeat this audit against the final tag
before release.
Remaining release work can still change the public surface. Reconcile this guide against the final public API and package diff after that work lands.
Who must migrate
Work through the complete guide if any of these apply:
- the dependency graph contains
Humanizer.Coreor aHumanizer.Core.<locale>package; - an
intreceives the result ofToNumber,TryToNumber, orIWordsToNumberConverter; - source code calls
ResourcesorResourceKeys, or a formatter subclass overrides the protected resource-key hooks onDefaultFormatter; - calls omit
CultureInfowhileCurrentCultureandCurrentUICulturecan differ; - tests, persisted labels, URLs, identifiers, or serialized output depend on exact Humanizer strings.
An application that starts directly on Humanizer 4 has no 3.0.10 compatibility work. Review the current feature documentation instead.
Upgrade checklist
- Create a migration branch and make the
3.0.10build and tests green. - Record every direct and transitive
Humanizer*package reference. - Remove
Humanizer.Coreand everyHumanizer.Core.<locale>reference. - Add only
Humanizerat the selected Humanizer 4 candidate version. - Delete stale restore outputs or regenerate the lock file, then restore.
- Build all consumers and fix the public API changes below.
- Run the words-to-number compiler code fix where it applies.
- Compare exact string output for every Humanizer operation and culture the application uses.
- Run the validation checklist on every supported target framework and build host.
Example
Package before and after
Humanizer 3.0.10 publishes a metapackage graph. Humanizer depends on 51
locale packages; each locale package depends on Humanizer.Core, which carries
the runtime and analyzers. The Humanizer 4 candidate publishes one Humanizer
package. Its assemblies contain the generated locale data, and the package
also contains the Roslyn 3.8, 4.8, and 4.14 analyzer variants.
<!-- 3.0.10: either the metapackage ... -->
<PackageReference Include="Humanizer" Version="3.0.10" />
<!-- ... or a manually selected core and locale graph -->
<PackageReference Include="Humanizer.Core" Version="3.0.10" />
<PackageReference Include="Humanizer.Core.fr" Version="3.0.10" />
<!-- Humanizer 4: replace both forms with one reference -->
<PackageReference Include="Humanizer" Version="<humanizer-4-version>" />
| Migration detail | What to do |
|---|---|
| Affected callers | Projects with direct Humanizer.Core or Humanizer.Core.<locale> references, central package pins, lock files, or allowlists for the old package IDs. |
| Before | Runtime, locale satellite assemblies, and analyzers arrive through a metapackage graph. |
| After | One Humanizer package supplies the runtime, generated locales, analyzers, and buildTransitive/Humanizer.targets. |
| Symptom | Leaving old references in place can select both the 3.x and 4.x graphs, retaining stale runtime or analyzer assets and producing restore or build conflicts. |
| Minimal action | Remove every old core/locale reference and version pin. Add one Humanizer 4 reference and regenerate restore outputs. |
| Verify | Run dotnet list <project> package --include-transitive; the result must contain Humanizer at one version and no Humanizer.Core* package. Inspect obj/project.assets.json when central package management or lock files hide the source. |
| Evidence | The 3.0.10 metapackage specification and the Humanizer 4 pack inputs. |
For netstandard2.0 and net48, the candidate raises its
System.Collections.Immutable dependency from 9.0.10 to 10.0.10;
System.ComponentModel.Annotations remains 5.0.0 and System.Memory remains
4.6.3. A centrally managed lower pin can produce NU1605. Remove that pin or
align it with the resolved Humanizer dependency, then inspect the restore graph.
Precompiled Humanizer consumers also need rebuilding. The strong-named
candidate assembly has version 4.0.0.0, compared with 3.0.0.0 in Humanizer
3.0.10; the public key token remains 979442b78dfc278e.
| Migration detail | What to do |
|---|---|
| Affected callers | Plugins, extensions, or libraries that consume Humanizer but are deployed as precompiled binaries, especially .NET Framework applications. |
| Before | The binary references strong-named Humanizer, Version=3.0.0.0. |
| After | The candidate binary is Humanizer, Version=4.0.0.0, with removed members and widened words-to-number signatures. |
| Symptom | Assembly-load, missing-type, or missing-member failures can occur before application code reaches the changed call site. |
| Minimal action | Rebuild every Humanizer-consuming binary against the selected candidate; do not carry forward a 3.x binding redirect as the migration. |
| Verify | Inspect the final output and dependency manifest, then load and exercise each plugin or extension on every deployment target. No Humanizer 3.x assembly may remain. |
| Evidence | Assembly metadata from the published 3.0.10 package and the package built from the candidate version configuration. |
Target frameworks and toolchain
No 3.0.10 target framework is removed. Humanizer 4 adds a net11.0 asset.
| Version | Package assets |
|---|---|
3.0.10 | netstandard2.0, net48, net8.0, net10.0 |
| Humanizer 4 candidate | netstandard2.0, net48, net8.0, net10.0, net11.0 |
Existing net6.0 and net7.0 consumers continue to select the
netstandard2.0 asset. Do not retarget an application merely to upgrade
Humanizer. If the application targets net11.0, use an SDK that supports that
target; otherwise keep the application's current target and SDK policy.
The Humanizer 4 package retains the three Roslyn-versioned analyzer assets used
by 3.0.10. Older build hosts still use the packaged fallback target. Verify
every CI image and IDE because analyzer selection is a build-host concern, not
a runtime TFM concern.
Building Humanizer itself at the audited candidate uses the SDK pinned in
global.json (11.0.100-preview.6.26359.118); 3.0.10 used 10.0.100.
Those are contributor toolchains, not consumer minimums. A consuming project
only needs an SDK compatible with its own target framework and build policy.
Public API changes
Change words-to-number results from int to long
ToNumber, both TryToNumber overloads, and all three
IWordsToNumberConverter members now use long.
// 3.0.10
int value = words.ToNumber(culture);
bool parsed = words.TryToNumber(out int result, culture);
// Humanizer 4 when the application accepts the wider contract
long value = words.ToNumber(culture);
bool parsed = words.TryToNumber(out long result, culture);
// Humanizer 4 when the application contract must remain Int32
int legacyValue = checked((int)words.ToNumber(culture));
| Migration detail | What to do |
|---|---|
| Affected callers | Explicit int assignments, out int declarations, method return values, generic constraints, mocks, and custom IWordsToNumberConverter implementations. |
| Before | Parser APIs return int; converter implementations expose int members. |
| After | Parser APIs return long; implementations must expose matching long members. |
| Symptom | Typical compiler failures are CS0029, CS0266, CS1503, CS0535, or CS0738. |
| Minimal action | Use long or var. Keep int only at an application boundary and narrow with checked((int)...). Update custom converter signatures and their overflow handling. |
| Verify | Test zero, negative values, int.MaxValue, int.MaxValue + 1L, and the largest phrase the application accepts. Confirm a checked 32-bit boundary throws instead of wrapping. |
| Evidence | The 3.0.10 int API and the candidate long API. |
Replace resource-key APIs and formatter resource hooks
Resources, ResourceKeys, and the protected string-keyed Format and
GetResourceKey hooks on DefaultFormatter are removed. Use the typed public
operation that owns the output. For example, replace resource-key lookup for a
time-unit symbol with ToSymbol:
// 3.0.10
var key = ResourceKeys.TimeUnitSymbol.GetResourceKey(TimeUnit.Day);
var symbol = Resources.GetResource(key, culture);
// Humanizer 4
var symbol = TimeUnit.Day.ToSymbol(culture);
For application-wide customization, implement the typed interface such as
IFormatter, ICollectionFormatter, INumberToWordsConverter, or
IOrdinalizer, then register that implementation during startup:
Configurator.Formatters.Register("en-custom", customFormatter);
| Migration detail | What to do |
|---|---|
| Affected callers | Direct resource access, reflection over resource keys, and DefaultFormatter subclasses that override the removed protected methods. |
| Before | Public resource names and string-key hooks expose Humanizer's storage layout. |
| After | Typed extension methods and formatter/converter interfaces are the public contract; locale data is generated into the assembly. |
| Symptom | CS0103 or CS0246 for resource types, CS0115 for removed overrides, or custom output no longer being reached. |
| Minimal action | Call the typed Humanizer operation. Move custom behavior to the narrowest typed interface and register it before the registry is first resolved. |
| Verify | Exercise every replaced operation with the intended culture. For registered formatters, test application startup and the first Humanizer call because registries freeze on first resolution. |
| Evidence | The 3.0.10 formatter hooks and resource types, compared with the candidate's typed formatter surface. |
The audited public API baselines show no other removal or signature replacement
between 3.0.10 and this candidate. Additive Humanizer 4 APIs are intentionally
not listed here.
Behavioral changes
Recheck identifier and title transformations
Existing string transformations deliberately correct separator, punctuation, title-case, and culture-dependent casing behavior:
| Call | 3.0.10 | Humanizer 4 candidate |
|---|---|---|
"Research&Development".Humanize() | Research development | Research & development |
"thisIs3.5mmLong".Titleize() | This Is 3 5mm Long | This Is 3.5mm Long |
"customer name 1".Pascalize() | CustomerName 1 | CustomerName1 |
"some.text.here".Pascalize() | Some.text.here | SomeTextHere |
"__customer_name".Camelize() | customerName | __customerName |
"a great movie".Transform(To.TitleCase) | a Great Movie | A Great Movie |
"IstanbulInput".Underscore() under tr-TR | ıstanbul_ınput | istanbul_input |
| Migration detail | What to do |
|---|---|
| Affected callers | Exact-output assertions and code that uses Humanizer output as identifiers, URLs, cache keys, database keys, or persisted labels. |
| Before | Ampersands are dropped, some separators survive identifier conversion, a leading article remains lowercase, and identifier casing follows the ambient culture. |
| After | Ampersands are tokens, spaces and dots are identifier boundaries, the first title word is capitalized, and identifier casing is culture-invariant. |
| Symptom | Snapshot failures, changed generated identifiers, or a lookup miss against a previously persisted Humanizer-derived value. |
| Minimal action | Rebaseline display-only output. For durable identifiers, preserve the old value in application code or run an explicit data migration instead of silently regenerating keys. |
| Verify | Compare representative punctuation, numeric suffixes, acronyms, and Turkish/Azeri casing under every operation the application uses. |
| Evidence | Candidate tests for ampersands and identifier transformations. |
Preserve authored enum metadata casing
Enum.Humanize(LetterCasing) now applies the requested casing only to text
derived from the enum member name. Description and Display metadata is
authored display text and is returned unchanged, including inside a flags
combination.
| Call | 3.0.10 | Humanizer 4 candidate |
|---|---|---|
Metadata "SpaceX and the Moon" with LetterCasing.Title | Spacex and the Moon | SpaceX and the Moon |
Metadata flag "SpaceX" combined with name-derived NameDerived, title case | metadata and names are recased | SpaceX and Name Derived |
| Migration detail | What to do |
|---|---|
| Affected callers | Enum and flags humanization that supplies LetterCasing for members with Description or Display metadata. |
| Before | Humanizer applies the requested casing to both metadata and name-derived text. |
| After | Authored metadata keeps its casing; name-derived text still receives the requested casing. |
| Symptom | Capitalization changes in labels and exact-output tests without a compile error. |
| Minimal action | Put the final desired casing in the metadata. Call ApplyCase explicitly afterward only when blanket recasing is an application requirement. |
| Verify | Cover metadata-backed, name-derived, and mixed flags values for every requested casing. |
| Evidence | The candidate enum metadata tests and mixed-flags test; 3.0.10 unconditionally applies casing. |
Recheck relative date output
Existing date operations intentionally change their text:
| Call | 3.0.10 | Humanizer 4 candidate |
|---|---|---|
date.Humanize(date) for DateOnly | now | localized date-only wording such as today |
A relative DateTime 14 days away | 14 days from now | 2 weeks from now |
A precision-relative DateTime 90 days ago | 2 months ago | 3 months ago |
A precision-relative DateTime 730 days ago | one year ago | 2 years ago |
| Migration detail | What to do |
|---|---|
| Affected callers | Exact relative-date assertions, persisted phrases, UI copy review, and custom formatters that supply date phrases. |
| Before | Equal DateOnly values reuse instant wording; distances below 28 days use days; exact month/year multiples can be rounded down. |
| After | Equal dates use a date-only phrase, distances from 7 through 27 days use weeks, and exact month/year multiples use the corrected unit count. |
| Symptom | Text changes without a compiler error. |
| Minimal action | Rebaseline display strings. If business logic depends on the text, replace that logic with date arithmetic before upgrading. |
| Verify | Cover equal dates, cross-year adjacent dates, 6/7 and 27/28 days, and 89/90/91 and 729/730/731 days in each used culture. |
| Evidence | Candidate same-date tests, precision-boundary tests, and the week-selection algorithm. |
Recheck month and year TimeSpan decomposition
When maxUnit is Month or Year and precision permits more units, the
remainder now includes weeks instead of expressing the entire remainder as
days.
| Call | 3.0.10 | Humanizer 4 candidate |
|---|---|---|
365 days, maxUnit: Year, precision 7 | 11 months, 30 days | 11 months, 4 weeks, 2 days |
60 days, maxUnit: Month, precision 7 | 1 month, 29 days | 1 month, 4 weeks, 1 day |
| Migration detail | What to do |
|---|---|
| Affected callers | TimeSpan.Humanize calls with maxUnit set to Month or Year, enough precision to include the remainder, and exact-output consumers. |
| Before | The remainder after approximate months or years is emitted in days. |
| After | Whole weeks are emitted before the remaining days. |
| Symptom | A week component appears in output that previously contained only months and days. |
| Minimal action | Rebaseline display text. Format the remainder in application code only when a days-only contract is required. |
| Verify | Cover spans around 30, 60, 365, and 366 days with the application's actual precision, maxUnit, and minUnit. |
| Evidence | Candidate month/year decomposition tests and the decomposition implementation. |
Treat metric decimals as a maximum
The decimals argument to ToMetric now sets the maximum number of
fractional digits. Trailing zeroes are omitted unless
MetricNumeralFormats.KeepTrailingZeros is supplied, and rounding can promote
the result to the next metric unit.
| Call | 3.0.10 | Humanizer 4 candidate |
|---|---|---|
123L.ToMetric(decimals: 3) | 123.000 | 123 |
1000d.ToMetric(decimals: 2) | 1.00k | 1k |
999.9d.ToMetric(decimals: 0) | 1000 | 1k |
| Migration detail | What to do |
|---|---|
| Affected callers | Fixed-width metric labels, exact snapshots, and parsers that assume the previous trailing-zero or unit-boundary form. |
| Before | decimals fixes the number of fractional digits and boundary rounding can remain in the smaller unit. |
| After | decimals is a maximum; trailing zeroes are optional and a rounded boundary promotes the metric prefix. |
| Symptom | Trailing zeroes disappear or the metric prefix changes near a rounding boundary. |
| Minimal action | Add MetricNumeralFormats.KeepTrailingZeros for fixed-width display. Accept promoted units or apply an explicit application format when the unit itself is contractual. |
| Verify | Test integral values, fractional values, and values immediately below and above every used metric boundary. |
| Evidence | Candidate format flag and rounding tests. |
Recheck byte-size and byte-rate values
Existing byte APIs change at large unit and value-object boundaries:
| Call | 3.0.10 | Humanizer 4 candidate |
|---|---|---|
ByteSize.FromBytes(1_000_000_000_000_000).ToString() | 909.49 TB | 1 PB |
byteRate.ToString() for 400 bytes over 1 second | Humanizer.ByteRate | 400 B/s |
| Equality of 400 bytes/10 seconds and 800 bytes/20 seconds | unequal references | equal normalized rates with equal hash codes |
| Migration detail | What to do |
|---|---|
| Affected callers | PB/EB-scale ByteSize display and code that logs, compares, hashes, or stores ByteRate values in sets or dictionaries. |
| Before | Automatic ByteSize formatting stops at TB; ByteRate inherits object text and reference identity. |
| After | Automatic formatting selects PB/EB where appropriate; ByteRate formats a humanized rate and compares normalized bytes per second. |
| Symptom | Large-size text changes, logs become human-readable, or distinct ByteRate instances collapse to one value key. |
| Minimal action | Request an explicit TB format when that unit is contractual. Use reference comparison or an identity comparer only when object identity, rather than rate value, is required. |
| Verify | Cover TB/PB/EB boundaries, ByteRate.ToString, equivalent rates with different intervals, and dictionary/set behavior. |
| Evidence | Candidate large-unit tests and byte-rate value tests. |
Rebaseline classic English inflection corrections
The existing classic Pluralize, Singularize, and ToQuantity APIs correct
several English rules. This section does not describe the unreleased localized
inflection surface covered by #1892.
| Call | 3.0.10 | Humanizer 4 candidate |
|---|---|---|
"software".Pluralize() | softwares | software |
"PERSON".Pluralize() | People | PEOPLE |
"arrives".Singularize() | arrife | arrive |
"taxes".Singularize() | taxis | tax |
"specimen".Singularize() | speciman | specimen |
| Migration detail | What to do |
|---|---|
| Affected callers | Existing English inflection and quantity calls whose output is asserted, persisted, parsed, or used as a key. |
| Before | Some uncountable, all-caps, -ves, -axes, and unrelated -men inputs use incorrect generic rules. |
| After | The corrected vocabulary preserves uncountables and casing and distinguishes the affected endings. |
| Symptom | Exact English word forms change without a compile error. |
| Minimal action | Accept and rebaseline corrected display output, or register an application-specific vocabulary rule during startup when domain language requires another form. |
| Verify | Exercise the application's uncountables, all-caps words, -ves, -axes, and unrelated -men inputs through all three existing APIs. |
| Evidence | Candidate vocabulary rules and regression tests. |
Localization and culture changes
Ambient formatting now follows CurrentCulture
When a culture argument is omitted or null, registry-backed formatter and
converter paths now resolve from CultureInfo.CurrentCulture.
LocaliserRegistry.ResolveForUiCulture() remains explicitly tied to
CurrentUICulture. So do ToHeading() when its culture is omitted and
ToMetric(..., MetricNumeralFormats.UseScaleWord) when selecting a localized
scale word.
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en-US");
CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("de-DE");
var words = 1.ToWords();
// 3.0.10 follows CurrentUICulture: "eins"
// Humanizer 4 follows CurrentCulture: "one"
| Migration detail | What to do |
|---|---|
| Affected callers | Registry-backed calls without an explicit culture in applications where formatting culture and UI resource culture differ. |
| Before | Default registries resolve formatting from CurrentUICulture. |
| After | Registry-backed formatting resolves from CurrentCulture; ResolveForUiCulture(), culture-omitting ToHeading(), and localized ToMetric scale words continue to use CurrentUICulture. |
| Symptom | Localized output changes language or number formatting even though the call site is unchanged. |
| Minimal action | Pass CultureInfo at the call site when output language is part of the contract. Otherwise align both ambient cultures at the request or job boundary. |
| Verify | Set the two ambient cultures to different values and exercise registry-backed numbers, ordinals, relative dates, durations, collections, and clock notation. Separately verify headings and localized metric scale words against CurrentUICulture. |
| Evidence | The candidate's registry resolution tests. |
Keep same-name custom cultures distinct
Formatter and converter registries now cache by CultureInfo instance instead
of culture name. Two custom en-US instances with different number or date
formats no longer reuse whichever formatter was resolved first.
| Migration detail | What to do |
|---|---|
| Affected callers | Applications that clone or construct multiple CultureInfo instances with the same name but different formatting properties, and code that deliberately primed the old registry cache. |
| Before | The first resolved culture instance wins for every later instance with the same culture name. |
| After | Each CultureInfo instance retains its own formatting identity. |
| Symptom | Output begins honoring the second instance's custom signs, separators, or date formatting instead of reusing the first instance's formatter. |
| Minimal action | Remove cache-priming workarounds. Reuse a single culture instance only when shared formatting is actually intended. |
| Verify | Resolve two same-name culture instances with visibly different number-format properties and confirm that each produces its own output. |
| Evidence | The candidate registry cache and same-name culture test. |
Rebaseline localized exact strings
Humanizer 4 replaces satellite-resource locale assemblies with generated locale
definitions and completes or corrects culture-specific behavior. These are
output changes, not a new package-reference requirement. For example,
new[] { "one", "two", "three" }.Humanize() under en-GB changes from the
fallback Oxford-comma form one, two, and three to the authored British form
one, two and three.
| Migration detail | What to do |
|---|---|
| Affected callers | Localized snapshots, persisted text, parsers that consume Humanizer output, and applications that relied on an English or parent-culture fallback. |
| Before | Locale satellite assemblies and resource-backed engines provide the 3.0.10 output set. |
| After | Generated locale profiles own formatting and converter behavior in the consolidated assembly. |
| Symptom | Corrected wording, punctuation, number scales, grammatical forms, or supported operations change exact output without a compile failure. |
| Minimal action | Inventory the cultures and operations the application actually uses, compare output side by side, and obtain language review before accepting persisted or user-facing changes. |
| Verify | For each used culture, cover relative dates, durations, collections, numbers and ordinals, parsing, data units, compass output, and clock notation as applicable. |
| Evidence | The candidate's exact locale-output matrix and British collection regression. |
The audited exact-output changes include these representative existing operations:
| Culture and affected callers | 3.0.10 | Humanizer 4 candidate | Verification and evidence |
|---|---|---|---|
en-GB collection humanization | one, two, and three | one, two and three | Check three-or-more-item collections. Regression |
pt and pt-BR number-to-words after a scale and exact hundred | quinze mil duzentos | quinze mil e duzentos | Check 15_200 and analogous scale boundaries. Regression |
pl durations, relative dates, and data units with compound paucal counts | 22 minut; za 22 minut; 22 bajtów | 22 minuty; za 22 minuty; 22 bajty | Check 12–14 and 21–25. Regression |
el feminine number-to-words | εννιακόσια; χίλια εννιακόσια | εννιακόσιες; χίλια εννιακόσιες | Check feminine 900 and 1900. Regression |
ru east/west intercardinal headings | forms such as востоко-северо-восток and западо-юго-запад | восток-северо-восток and запад-юго-запад | Check all four affected headings. Locale data |
bs, hr, sl, sr, and sr-Latn feminine ordinal words | numeric fallback such as 1 | authored word such as prva | Check several genders and compound values. Coverage |
Do not infer localized-inflection behavior from the current candidate. The final #1892 API, resolver, generated data, and package diff must be audited and added here only after that issue is complete.
Analyzer-assisted changes
Humanizer 4 adds no new diagnostic ID for this migration. The packaged
words-to-number code fix attaches to compiler diagnostics CS0029 and CS0266
when the failing expression is a Humanizer ToNumber call or an
IWordsToNumberConverter.Convert call. In an IDE, choose Wrap with checked
int cast only where the application contract must remain 32-bit.
HUMANIZER001 still finds Humanizer 2 namespace usage. A clean 3.0.10
consumer should not need it for the Humanizer 4 migration, and it does not fix
package references, removed resource APIs, custom converter signatures, or
behavioral output changes.
Verify the analyzer package itself by confirming that the selected build host
loads one Humanizer.Analyzers.dll and reports neither CS8032 nor AD0001.
The analyzer guide covers build-host diagnostics and
severity configuration.
Validation checklist
Run these checks after source changes, then repeat them on the final Humanizer 4 tag:
dotnet clean <solution>
dotnet restore <solution> --force-evaluate
dotnet list <project> package --include-transitive
dotnet build <solution>
dotnet test <solution>
- The package list contains one Humanizer version and no
Humanizer.Core*. - Every target framework and CI build host restores and compiles.
- Custom
IWordsToNumberConverterimplementations exposelongmembers. - Any intentional 32-bit narrowing is checked and boundary-tested.
- No source references
Resources,ResourceKeys, or the removedDefaultFormatterresource-key hooks. - Analyzer loading emits no
CS8032orAD0001. - Identifier, title, enum, date, duration, metric, byte, and classic-inflection output was reviewed.
ByteRateequality and hashing were reviewed anywhere values are used as keys.CurrentCultureandCurrentUICulturewere tested separately.- Same-name custom
CultureInfoinstances were tested separately when used. - Every used culture and applicable localized operation has reviewed output.