Skip to main content
Version: 4.0 (next)

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.

Release candidate boundary

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.Core or a Humanizer.Core.<locale> package;
  • an int receives the result of ToNumber, TryToNumber, or IWordsToNumberConverter;
  • source code calls Resources or ResourceKeys, or a formatter subclass overrides the protected resource-key hooks on DefaultFormatter;
  • calls omit CultureInfo while CurrentCulture and CurrentUICulture can 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

  1. Create a migration branch and make the 3.0.10 build and tests green.
  2. Record every direct and transitive Humanizer* package reference.
  3. Remove Humanizer.Core and every Humanizer.Core.<locale> reference.
  4. Add only Humanizer at the selected Humanizer 4 candidate version.
  5. Delete stale restore outputs or regenerate the lock file, then restore.
  6. Build all consumers and fix the public API changes below.
  7. Run the words-to-number compiler code fix where it applies.
  8. Compare exact string output for every Humanizer operation and culture the application uses.
  9. 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.

Application.csproj
<!-- 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 detailWhat to do
Affected callersProjects with direct Humanizer.Core or Humanizer.Core.<locale> references, central package pins, lock files, or allowlists for the old package IDs.
BeforeRuntime, locale satellite assemblies, and analyzers arrive through a metapackage graph.
AfterOne Humanizer package supplies the runtime, generated locales, analyzers, and buildTransitive/Humanizer.targets.
SymptomLeaving 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 actionRemove every old core/locale reference and version pin. Add one Humanizer 4 reference and regenerate restore outputs.
VerifyRun 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.
EvidenceThe 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 detailWhat to do
Affected callersPlugins, extensions, or libraries that consume Humanizer but are deployed as precompiled binaries, especially .NET Framework applications.
BeforeThe binary references strong-named Humanizer, Version=3.0.0.0.
AfterThe candidate binary is Humanizer, Version=4.0.0.0, with removed members and widened words-to-number signatures.
SymptomAssembly-load, missing-type, or missing-member failures can occur before application code reaches the changed call site.
Minimal actionRebuild every Humanizer-consuming binary against the selected candidate; do not carry forward a 3.x binding redirect as the migration.
VerifyInspect 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.
EvidenceAssembly 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.

VersionPackage assets
3.0.10netstandard2.0, net48, net8.0, net10.0
Humanizer 4 candidatenetstandard2.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 detailWhat to do
Affected callersExplicit int assignments, out int declarations, method return values, generic constraints, mocks, and custom IWordsToNumberConverter implementations.
BeforeParser APIs return int; converter implementations expose int members.
AfterParser APIs return long; implementations must expose matching long members.
SymptomTypical compiler failures are CS0029, CS0266, CS1503, CS0535, or CS0738.
Minimal actionUse long or var. Keep int only at an application boundary and narrow with checked((int)...). Update custom converter signatures and their overflow handling.
VerifyTest 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.
EvidenceThe 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 detailWhat to do
Affected callersDirect resource access, reflection over resource keys, and DefaultFormatter subclasses that override the removed protected methods.
BeforePublic resource names and string-key hooks expose Humanizer's storage layout.
AfterTyped extension methods and formatter/converter interfaces are the public contract; locale data is generated into the assembly.
SymptomCS0103 or CS0246 for resource types, CS0115 for removed overrides, or custom output no longer being reached.
Minimal actionCall the typed Humanizer operation. Move custom behavior to the narrowest typed interface and register it before the registry is first resolved.
VerifyExercise every replaced operation with the intended culture. For registered formatters, test application startup and the first Humanizer call because registries freeze on first resolution.
EvidenceThe 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:

Call3.0.10Humanizer 4 candidate
"Research&Development".Humanize()Research developmentResearch & development
"thisIs3.5mmLong".Titleize()This Is 3 5mm LongThis Is 3.5mm Long
"customer name 1".Pascalize()CustomerName 1CustomerName1
"some.text.here".Pascalize()Some.text.hereSomeTextHere
"__customer_name".Camelize()customerName__customerName
"a great movie".Transform(To.TitleCase)a Great MovieA Great Movie
"IstanbulInput".Underscore() under tr-TRıstanbul_ınputistanbul_input
Migration detailWhat to do
Affected callersExact-output assertions and code that uses Humanizer output as identifiers, URLs, cache keys, database keys, or persisted labels.
BeforeAmpersands are dropped, some separators survive identifier conversion, a leading article remains lowercase, and identifier casing follows the ambient culture.
AfterAmpersands are tokens, spaces and dots are identifier boundaries, the first title word is capitalized, and identifier casing is culture-invariant.
SymptomSnapshot failures, changed generated identifiers, or a lookup miss against a previously persisted Humanizer-derived value.
Minimal actionRebaseline display-only output. For durable identifiers, preserve the old value in application code or run an explicit data migration instead of silently regenerating keys.
VerifyCompare representative punctuation, numeric suffixes, acronyms, and Turkish/Azeri casing under every operation the application uses.
EvidenceCandidate 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.

Call3.0.10Humanizer 4 candidate
Metadata "SpaceX and the Moon" with LetterCasing.TitleSpacex and the MoonSpaceX and the Moon
Metadata flag "SpaceX" combined with name-derived NameDerived, title casemetadata and names are recasedSpaceX and Name Derived
Migration detailWhat to do
Affected callersEnum and flags humanization that supplies LetterCasing for members with Description or Display metadata.
BeforeHumanizer applies the requested casing to both metadata and name-derived text.
AfterAuthored metadata keeps its casing; name-derived text still receives the requested casing.
SymptomCapitalization changes in labels and exact-output tests without a compile error.
Minimal actionPut the final desired casing in the metadata. Call ApplyCase explicitly afterward only when blanket recasing is an application requirement.
VerifyCover metadata-backed, name-derived, and mixed flags values for every requested casing.
EvidenceThe 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:

Call3.0.10Humanizer 4 candidate
date.Humanize(date) for DateOnlynowlocalized date-only wording such as today
A relative DateTime 14 days away14 days from now2 weeks from now
A precision-relative DateTime 90 days ago2 months ago3 months ago
A precision-relative DateTime 730 days agoone year ago2 years ago
Migration detailWhat to do
Affected callersExact relative-date assertions, persisted phrases, UI copy review, and custom formatters that supply date phrases.
BeforeEqual DateOnly values reuse instant wording; distances below 28 days use days; exact month/year multiples can be rounded down.
AfterEqual dates use a date-only phrase, distances from 7 through 27 days use weeks, and exact month/year multiples use the corrected unit count.
SymptomText changes without a compiler error.
Minimal actionRebaseline display strings. If business logic depends on the text, replace that logic with date arithmetic before upgrading.
VerifyCover 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.
EvidenceCandidate 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.

Call3.0.10Humanizer 4 candidate
365 days, maxUnit: Year, precision 711 months, 30 days11 months, 4 weeks, 2 days
60 days, maxUnit: Month, precision 71 month, 29 days1 month, 4 weeks, 1 day
Migration detailWhat to do
Affected callersTimeSpan.Humanize calls with maxUnit set to Month or Year, enough precision to include the remainder, and exact-output consumers.
BeforeThe remainder after approximate months or years is emitted in days.
AfterWhole weeks are emitted before the remaining days.
SymptomA week component appears in output that previously contained only months and days.
Minimal actionRebaseline display text. Format the remainder in application code only when a days-only contract is required.
VerifyCover spans around 30, 60, 365, and 366 days with the application's actual precision, maxUnit, and minUnit.
EvidenceCandidate 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.

Call3.0.10Humanizer 4 candidate
123L.ToMetric(decimals: 3)123.000123
1000d.ToMetric(decimals: 2)1.00k1k
999.9d.ToMetric(decimals: 0)10001k
Migration detailWhat to do
Affected callersFixed-width metric labels, exact snapshots, and parsers that assume the previous trailing-zero or unit-boundary form.
Beforedecimals fixes the number of fractional digits and boundary rounding can remain in the smaller unit.
Afterdecimals is a maximum; trailing zeroes are optional and a rounded boundary promotes the metric prefix.
SymptomTrailing zeroes disappear or the metric prefix changes near a rounding boundary.
Minimal actionAdd MetricNumeralFormats.KeepTrailingZeros for fixed-width display. Accept promoted units or apply an explicit application format when the unit itself is contractual.
VerifyTest integral values, fractional values, and values immediately below and above every used metric boundary.
EvidenceCandidate format flag and rounding tests.

Recheck byte-size and byte-rate values

Existing byte APIs change at large unit and value-object boundaries:

Call3.0.10Humanizer 4 candidate
ByteSize.FromBytes(1_000_000_000_000_000).ToString()909.49 TB1 PB
byteRate.ToString() for 400 bytes over 1 secondHumanizer.ByteRate400 B/s
Equality of 400 bytes/10 seconds and 800 bytes/20 secondsunequal referencesequal normalized rates with equal hash codes
Migration detailWhat to do
Affected callersPB/EB-scale ByteSize display and code that logs, compares, hashes, or stores ByteRate values in sets or dictionaries.
BeforeAutomatic ByteSize formatting stops at TB; ByteRate inherits object text and reference identity.
AfterAutomatic formatting selects PB/EB where appropriate; ByteRate formats a humanized rate and compares normalized bytes per second.
SymptomLarge-size text changes, logs become human-readable, or distinct ByteRate instances collapse to one value key.
Minimal actionRequest 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.
VerifyCover TB/PB/EB boundaries, ByteRate.ToString, equivalent rates with different intervals, and dictionary/set behavior.
EvidenceCandidate 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.

Call3.0.10Humanizer 4 candidate
"software".Pluralize()softwaressoftware
"PERSON".Pluralize()PeoplePEOPLE
"arrives".Singularize()arrifearrive
"taxes".Singularize()taxistax
"specimen".Singularize()specimanspecimen
Migration detailWhat to do
Affected callersExisting English inflection and quantity calls whose output is asserted, persisted, parsed, or used as a key.
BeforeSome uncountable, all-caps, -ves, -axes, and unrelated -men inputs use incorrect generic rules.
AfterThe corrected vocabulary preserves uncountables and casing and distinguishes the affected endings.
SymptomExact English word forms change without a compile error.
Minimal actionAccept and rebaseline corrected display output, or register an application-specific vocabulary rule during startup when domain language requires another form.
VerifyExercise the application's uncountables, all-caps words, -ves, -axes, and unrelated -men inputs through all three existing APIs.
EvidenceCandidate 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 detailWhat to do
Affected callersRegistry-backed calls without an explicit culture in applications where formatting culture and UI resource culture differ.
BeforeDefault registries resolve formatting from CurrentUICulture.
AfterRegistry-backed formatting resolves from CurrentCulture; ResolveForUiCulture(), culture-omitting ToHeading(), and localized ToMetric scale words continue to use CurrentUICulture.
SymptomLocalized output changes language or number formatting even though the call site is unchanged.
Minimal actionPass CultureInfo at the call site when output language is part of the contract. Otherwise align both ambient cultures at the request or job boundary.
VerifySet 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.
EvidenceThe 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 detailWhat to do
Affected callersApplications that clone or construct multiple CultureInfo instances with the same name but different formatting properties, and code that deliberately primed the old registry cache.
BeforeThe first resolved culture instance wins for every later instance with the same culture name.
AfterEach CultureInfo instance retains its own formatting identity.
SymptomOutput begins honoring the second instance's custom signs, separators, or date formatting instead of reusing the first instance's formatter.
Minimal actionRemove cache-priming workarounds. Reuse a single culture instance only when shared formatting is actually intended.
VerifyResolve two same-name culture instances with visibly different number-format properties and confirm that each produces its own output.
EvidenceThe 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 detailWhat to do
Affected callersLocalized snapshots, persisted text, parsers that consume Humanizer output, and applications that relied on an English or parent-culture fallback.
BeforeLocale satellite assemblies and resource-backed engines provide the 3.0.10 output set.
AfterGenerated locale profiles own formatting and converter behavior in the consolidated assembly.
SymptomCorrected wording, punctuation, number scales, grammatical forms, or supported operations change exact output without a compile failure.
Minimal actionInventory the cultures and operations the application actually uses, compare output side by side, and obtain language review before accepting persisted or user-facing changes.
VerifyFor each used culture, cover relative dates, durations, collections, numbers and ordinals, parsing, data units, compass output, and clock notation as applicable.
EvidenceThe candidate's exact locale-output matrix and British collection regression.

The audited exact-output changes include these representative existing operations:

Culture and affected callers3.0.10Humanizer 4 candidateVerification and evidence
en-GB collection humanizationone, two, and threeone, two and threeCheck three-or-more-item collections. Regression
pt and pt-BR number-to-words after a scale and exact hundredquinze mil duzentosquinze mil e duzentosCheck 15_200 and analogous scale boundaries. Regression
pl durations, relative dates, and data units with compound paucal counts22 minut; za 22 minut; 22 bajtów22 minuty; za 22 minuty; 22 bajtyCheck 12–14 and 21–25. Regression
el feminine number-to-wordsεννιακόσια; χίλια εννιακόσιαεννιακόσιες; χίλια εννιακόσιεςCheck feminine 900 and 1900. Regression
ru east/west intercardinal headingsforms such as востоко-северо-восток and западо-юго-западвосток-северо-восток and запад-юго-западCheck all four affected headings. Locale data
bs, hr, sl, sr, and sr-Latn feminine ordinal wordsnumeric fallback such as 1authored word such as prvaCheck 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 IWordsToNumberConverter implementations expose long members.
  • Any intentional 32-bit narrowing is checked and boundary-tested.
  • No source references Resources, ResourceKeys, or the removed DefaultFormatter resource-key hooks.
  • Analyzer loading emits no CS8032 or AD0001.
  • Identifier, title, enum, date, duration, metric, byte, and classic-inflection output was reviewed.
  • ByteRate equality and hashing were reviewed anywhere values are used as keys.
  • CurrentCulture and CurrentUICulture were tested separately.
  • Same-name custom CultureInfo instances were tested separately when used.
  • Every used culture and applicable localized operation has reviewed output.