Skip to content

Parsing and Validation

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

Enbrea.Bcp47 separates RFC 5646 grammar checking, registry-independent structural validation, and optional IANA registry validation.

This distinction is intentional: a language tag can satisfy the RFC 5646 grammar without every subtag being registered in the IANA Language Subtag Registry.

Parsing a language tag

The most convenient entry point is LanguageTag.Parse():

var tag = LanguageTag.Parse("zh-Hans-CN");

The equivalent parser API is:

var tag = Bcp47Parser.Parse("zh-Hans-CN");

Both methods require a structurally valid language tag and throw FormatException if the input cannot be parsed.

For input that may be invalid, use TryParse():

if (LanguageTag.TryParse(value, out var tag))
{
    Console.WriteLine(tag);
}

The equivalent lower-level call is:

if (Bcp47Parser.TryParse(value, out var tag))
{
    Console.WriteLine(tag);
}

Inspecting components

A parsed LanguageTag exposes normalized components:

var tag = LanguageTag.Parse("ZH-cmn-hans-cn-variant-a-aaa-x-private");

Console.WriteLine(tag.Language); // zh
Console.WriteLine(tag.Script);   // Hans
Console.WriteLine(tag.Region);   // CN

Collection-valued components are available through:

tag.Extlangs
tag.Variants
tag.Extensions
tag.PrivateUse

For example:

var tag = LanguageTag.Parse("zh-cmn-Hans-CN-x-example");

foreach (var extlang in tag.Extlangs)
{
    Console.WriteLine(extlang);
}

foreach (var privateUse in tag.PrivateUse)
{
    Console.WriteLine(privateUse);
}

Each extension is represented by LanguageTagExtension:

var tag = LanguageTag.Parse("en-u-ca-gregory");
var extension = tag.Extensions[0];

Console.WriteLine(extension.Singleton); // u

foreach (var subtag in extension.Subtags)
{
    Console.WriteLine(subtag);
}

Original and normalized values

LanguageTag.Value preserves the original input exactly:

var tag = LanguageTag.Parse("ZH-hans-cn");

Console.WriteLine(tag.Value); // ZH-hans-cn

ToString() returns the registry-independent normalized form:

Console.WriteLine(tag.ToString()); // zh-Hans-CN

See Normalization and Equality for the normalization rules.

Well-formed language tags

IsWellFormed() checks the RFC 5646 grammar:

bool result = LanguageTag.IsWellFormed("de-DE");

The parser API provides the same operation:

bool result = Bcp47Parser.IsWellFormed("de-DE");

Well-formedness intentionally follows the grammar only. RFC 5646 grammar permits up to three extended-language positions, for example:

LanguageTag.IsWellFormed("aaa-bbb-ccc-ddd"); // true

That does not mean the tag satisfies all registry-independent validity constraints.

Structural validation

IsStructurallyValid() adds registry-independent validity checks on top of the grammar:

LanguageTag.IsStructurallyValid("de-DE"); // true

Structural validation additionally rejects cases such as:

  • more than one extlang subtag
  • duplicate variant subtags
  • duplicate extension singletons

For example:

LanguageTag.IsWellFormed("aaa-bbb-ccc-ddd");         // true
LanguageTag.IsStructurallyValid("aaa-bbb-ccc-ddd");  // false

LanguageTag.IsWellFormed("sl-rozaj-rozaj");          // true
LanguageTag.IsStructurallyValid("sl-rozaj-rozaj");   // false

LanguageTag.IsWellFormed("en-a-aaa-a-bbb");          // true
LanguageTag.IsStructurallyValid("en-a-aaa-a-bbb");   // false

Registry-independent means registry-independent

Structural validation does not ask whether a syntactically valid subtag is present in the IANA registry.

For example, a four-letter subtag in script position satisfies the script grammar independently of whether that script is registered:

LanguageTag.IsStructurallyValid("de-Abcd");

Use Enbrea.Bcp47.Iana when registry membership matters. See IANA Validation.

Private-use-only tags

A tag can consist entirely of private-use subtags:

var tag = LanguageTag.Parse("x-company-internal");

Console.WriteLine(tag.IsPrivateUseOnly); // true
Console.WriteLine(tag.Language);         // null

The private-use subtags are exposed separately:

foreach (var value in tag.PrivateUse)
{
    Console.WriteLine(value);
}

Grandfathered tags

RFC 5646 preserves a fixed set of grandfathered language tags.

They are recognized before the regular language-tag grammar is applied:

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

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

Grandfathered tags are treated as complete tags and are not decomposed into ordinary language, script, region, and variant components.

Exceptions

Parse() and Normalize() throw:

  • ArgumentNullException for null
  • FormatException for invalid language-tag syntax or structure

Use the corresponding Try... methods when invalid input is expected.

See also:

Clone this wiki locally