Skip to content

Normalization and Equality

Frank Stüber edited this page Sep 11, 2026 · 5 revisions

Enbrea.Bcp47 normalizes language tags without consulting the IANA Language Subtag Registry.

This provides deterministic casing and ordering while keeping the core package independent of registry snapshots and Preferred-Value mappings.

Normalizing a language tag

Use LanguageTag.Normalize():

var normalized = LanguageTag.Normalize("ZH-hans-cn");

Console.WriteLine(normalized); // zh-Hans-CN

The equivalent parser API is:

var normalized = Bcp47Parser.Normalize("ZH-hans-cn");

Use TryNormalize() when invalid input is expected:

if (LanguageTag.TryNormalize(value, out var normalized))
{
    Console.WriteLine(normalized);
}

Casing rules

Normalization applies the conventional BCP 47 casing by component:

Component Normalized form
Language lowercase
Extlang lowercase
Script title case
Alphabetic region uppercase
Numeric region unchanged
Variant lowercase
Extension singleton and subtags lowercase
Private use lowercase

For example:

var normalized = LanguageTag.Normalize("ZH-CMN-hANS-cn-VARIANT-X-PRIVATE");

Console.WriteLine(normalized); // zh-cmn-Hans-CN-variant-x-private

Extension ordering

Extension sequences are normalized into deterministic singleton order.

For example:

var normalized = LanguageTag.Normalize("en-b-bbb-a-aaa");

Console.WriteLine(normalized); // en-a-aaa-b-bbb

This means normalization is not only a casing operation.

Grandfathered tags

Grandfathered tags are normalized to their registered spelling:

var tag = LanguageTag.Parse("I-KLINGON");

Console.WriteLine(tag.Value); // I-KLINGON
Console.WriteLine(tag);       // i-klingon

They are not decomposed into ordinary components.

Original versus normalized representation

A parsed tag retains both concepts:

var tag = LanguageTag.Parse("DE-de");

Console.WriteLine(tag.Value); // DE-de
Console.WriteLine(tag);       // de-DE

Use Value when the original input matters. Use ToString() for the normalized representation.

Equality

LanguageTag implements IEquatable<LanguageTag> and compares tags by normalized representation.

For example:

var first = LanguageTag.Parse("DE-de");
var second = LanguageTag.Parse("de-DE");

Console.WriteLine(first == second);       // true
Console.WriteLine(first.Equals(second));  // true

Equal tags also produce compatible hash codes, so they can be used as keys in dictionaries or values in hash sets.

var tags = new HashSet<LanguageTag>
{
    LanguageTag.Parse("DE-de"),
    LanguageTag.Parse("de-DE")
};

Console.WriteLine(tags.Count); // 1

Registry-independent normalization is not IANA canonicalization

Core normalization deliberately does not apply registry metadata such as:

  • Preferred-Value
  • Suppress-Script
  • deprecation state
  • macrolanguage relationships

For example, if an old registered language subtag has a preferred replacement, LanguageTag.Normalize() does not replace it automatically.

Use Enbrea.Bcp47.Iana to inspect such metadata:

using Enbrea.Bcp47.Iana;

if (IanaLanguageSubtagRegistry.TryGetPreferredValue(IanaLanguageSubtagType.Language, "iw", out var preferred))
{
    Console.WriteLine(preferred);
}

Keeping normalization and registry metadata separate makes the result of core parsing independent of the bundled registry snapshot.

See IANA Registry and IANA Validation.

Clone this wiki locally