Humanizer supports many languages and cultures across number, date, time, ordinal, formatter, collection-humanization, clock-notation, and heading surfaces.
Most consumers only need to set a culture. Contributors now author locale-specific generated behavior in one YAML file per locale, and shared runtime kernels are used whenever the behavior is structurally reusable.
Humanizer includes localization for:
Afrikaans (af), Arabic (ar), Azerbaijani (az), Bengali (bn), Bulgarian (bg), Catalan (ca), Chinese (zh-CN, zh-Hans, zh-Hant), Croatian (hr), Czech (cs), Danish (da), Dutch (nl), English (en, en-GB, en-IN, en-US), Finnish (fi), Filipino (fil), French (fr, fr-BE, fr-CH), German (de, de-CH, de-LI), Greek (el), Hebrew (he), Hungarian (hu), Armenian (hy), Icelandic (is), Indonesian (id), Italian (it), Japanese (ja), Korean (ko), Kurdish (ku), Latvian (lv), Lithuanian (lt), Luxembourgish (lb), Malay (ms), Maltese (mt), Norwegian Bokmal (nb), Norwegian Nynorsk (nn), Persian (fa), Polish (pl), Portuguese (pt, pt-BR), Romanian (ro), Russian (ru), Serbian (sr, sr-Latn), Slovak (sk), Slovenian (sl), Spanish (es), Swedish (sv), Tamil (ta), Thai (th), Turkish (tr), Ukrainian (uk), Uzbek (uz-Cyrl-UZ, uz-Latn-UZ), Vietnamese (vi), Zulu (zu-ZA).
dotnet add package HumanizerThe Humanizer package already includes all generated locale data.
Most Humanizer methods respect the current thread's CurrentCulture or CurrentUICulture. You can also explicitly specify a culture:
var date = DateTime.UtcNow.AddHours(-2);
date.Humanize();
date.Humanize(culture: new CultureInfo("fr-FR"));
date.Humanize(culture: new CultureInfo("es"));
1234.ToWords();
1234.ToWords(new CultureInfo("es"));
1234.ToNumber(new CultureInfo("es"));Some languages require additional grammatical information:
1.ToWords(GrammaticalGender.Masculine, new CultureInfo("ru"));
1.ToWords(GrammaticalGender.Feminine, new CultureInfo("ru"));
1.Ordinalize(GrammaticalGender.Masculine);
1.Ordinalize(GrammaticalGender.Feminine);
var date = new DateTime(2020, 1, 1);
date.ToOrdinalWords(GrammaticalCase.Nominative);
date.ToOrdinalWords(GrammaticalCase.Genitive);Localized Humanizer features are expected to work correctly for shipped locales. Whether a locale inherits behavior from a same-language parent is an implementation detail, not a support distinction.
Contributor-facing parity audits and gap tracking live in tests and local planning artifacts rather than in the user docs.
The source of truth for generated localization behavior is src/Humanizer/Locales/<locale>.yml.
For the practical authoring workflow, see Locale YAML How-To. For the exhaustive block, engine, field, and strategy reference, see Locale YAML Reference.
Principles:
- One locale file owns one locale.
- Locale inheritance is declared in that same file with
variantOf. - Top-level properties are exactly
locale,variantOf, andsurfaces. - Canonical authoring surfaces under
surfacesare exactlylist,formatter,phrases,number,ordinal,clock,compass, andcalendar. - Canonical nested members are
number.words,number.parse,number.formatting,ordinal.numeric,ordinal.date,ordinal.dateOnly,calendar.months, andcalendar.monthsGenitive. - Omit a
surfaces.<surface>block to inherit it unchanged from the parent locale. - Inside a mapped surface, omit unchanged fields to inherit them from the parent mapping.
- Child sequences replace parent sequences.
- Changing
enginereplaces that mapped surface instead of merging it. - Keep locale-specific words, switches, and mappings in YAML.
- Keep
number.wordsandnumber.parsealigned whenever the locale claims parity. formatterandphrasesare separate canonical surfaces and should not be collapsed into one conceptual bucket.clockis the canonical authoring name even though the emitted runtime feature is stilltimeOnlyToClockNotation.compassis the canonical authoring name even though the emitted runtime feature is stillheadings.- Keep shared generator contracts in typed C# under
src/Humanizer.SourceGenerators/Common/EngineContractCatalog.cs. - Never make authors learn internal generated profile ids just to connect two features inside one locale file.
- The current authoring model keeps locale YAML at the top level of
src/Humanizer/Locales. - Do not split one locale across multiple YAML files unless there is an explicit redesign that updates the compiler contract, docs, and tests together.
A shipped locale is incomplete unless every canonical surface is explicitly accounted for as locale-owned or same-language inherited with proof. There is no shipped-locale exemption list in this repo.
Example:
# Locale-owned generator data for en-US.
locale: 'en-US'
variantOf: 'en'
surfaces:
list:
engine: 'oxford'
number:
words:
engine: 'conjunctional-scale'
minusWord: 'minus'
andWord: 'and'
unitsMap:
- 'zero'
- 'one'
- 'two'
parse:
engine: 'token-map'
normalizationProfile: 'LowercaseRemovePeriods'
cardinalMap:
one: 1
two: 2
hundred: 100
ordinalNumberToWordsKind: 'self'surfaces.number.parse.ordinalNumberToWordsKind: 'self' is intentional. Locale YAML is authored in locale terms, not in generator-internal profile-key terms. The generator resolves self to the owning locale profile during code generation.
The localization codegen flow is:
Locales/*.ymlLocale-owned authoring surface for all generated features.LocaleYamlCatalogParses canonical YAML, resolvesvariantOfinheritance, and exposes per-locale feature roots.EngineContractCatalogTyped generator-side engine contracts that describe how a feature block maps onto a runtime profile object.ProfileCatalogInputgenerators Build typed profile catalogs and tables fornumberToWords,wordsToNumber,ordinalizer,date-to-ordinal,formatter,phrases,headings, andtimeOnlyToClockNotation.LocaleRegistryInputEmits the culture-to-implementation registrations that wire generated profiles into the runtime registries.- Shared runtime kernels Consume the generated profile objects at runtime with no YAML or JSON parsing on the hot path.
This split is deliberate:
- YAML is for locale-owned data.
- Generator-side structural contracts are typed C#.
- C# runtime code is for shared algorithms.
Shared kernels must be named after the reusable rule family, not after the first language that used them.
Good structural names:
ConjunctionalScaleNumberToWordsConverterVariantDecadeNumberToWordsConverterUnitLeadingCompoundNumberToWordsConverterContractedScaleWordsToNumberConverterProfiledFormatter
Residual locale names are acceptable only when the behavior is still genuinely locale-specific and forcing it into a shared schema would create imperative hooks or exception-bucket metadata. As of the locale parity completion, no residual leaves remain for any surface.
When a locale already fits an existing shared engine:
- Produce a preflight gap report covering every canonical surface.
- Create or update
src/Humanizer/Locales/<locale>.yml. - Add
variantOfif the locale is a regional variant. - Fill in the
surfacesblocks that differ from the parent. - Reuse an existing structural engine name.
- Add tests under
tests/Humanizer.Tests. - Maintain a parity artifact until the unresolved set is empty.
- Run the relevant source-generator tests and localization tests.
When a locale does not fit an existing shared engine:
- Prove the behavior is shared by at least two locales, or by one locale plus an obvious second target already present in the repo.
- Add or update the typed generator contract in
src/Humanizer.SourceGenerators/Common/EngineContractCatalog.cs. - Add a shared runtime kernel with a structural name.
- Keep the runtime parse-free. YAML is build input only; the generator contract is normal checked-in code.
- Document the decision in the adjacent locale docs and code comments so the rationale stays with the implementation rather than in a stale execution plan.
Do not add a locale-specific converter just to avoid extending a clearly reusable shared family. Do not add a generic-sounding name if the implementation still hardcodes one language's rules. Do not split locale-owned YAML into extra files just to make the top-level locale file look smaller. If nested locale YAML documents ever become necessary, treat that as an intentional redesign instead of an incremental cleanup.
Every functional localization change should include:
- Source-generator coverage in
tests/Humanizer.SourceGenerators.Tests. - Locale behavior coverage in
tests/Humanizer.Tests. - Full
net10.0andnet8.0test runs for touched functionality. - Benchmark coverage for shared-engine surfaces when the change affects runtime dispatch or hot-path composition.
Recommended verification commands:
dotnet test tests/Humanizer.SourceGenerators.Tests/Humanizer.SourceGenerators.Tests.csproj --framework net10.0
dotnet test tests/Humanizer.Tests/Humanizer.Tests.csproj --framework net10.0
dotnet test tests/Humanizer.Tests/Humanizer.Tests.csproj --framework net8.0
dotnet pack src/Humanizer/Humanizer.csproj -c Release -o artifacts/plan-validationModern .NET uses ICU for globalization data while .NET Framework uses Windows NLS. These sources can diverge across platforms and target frameworks, producing different output for the same locale. When Humanizer delegates to CultureInfo for month names, decimal separators, negative signs, or group separators, this platform variance leaks into humanized output.
The calendar: surface and number.formatting: sub-block let locale authors hard-code the correct values in YAML so that output is byte-identical regardless of the host's globalization source or version. When present, calendar overrides take priority over CultureInfo.DateTimeFormat in ordinal date rendering (DateTime.ToOrdinalWords and DateOnly.ToOrdinalWords). Number formatting overrides take priority over NumberFormatInfo in culture-aware Ordinalize int overloads (formatting only), byte-size string formatting (ByteSize.ToString and ByteSize.ToFullWords), and MetricNumeralExtensions. ByteSize.TryParse applies only the decimal separator override, and only when an explicit CultureInfo is passed as the format provider; it does not use negativeSign or groupSeparator overrides. Caller-supplied custom format providers are never overridden.
Use these overrides sparingly. Author them only when a cross-platform or cross-target-framework probe shows disagreement (e.g., net48/NLS vs net8+/ICU on Windows), or when platform globalization data is demonstrably wrong for your locale. See Locale YAML Reference for field details and Locale YAML How-To for a step-by-step recipe.