Skip to content

Language Ranges

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

Language ranges are defined by RFC 4647 and are used to select or filter language tags.

A language range is not a LanguageTag. In particular, extended ranges can contain the wildcard * in individual subtag positions. Enbrea.Bcp47 therefore models ranges separately using LanguageRange and LanguageRangeParser.

Basic language ranges

A basic range contains a primary language-range subtag followed by zero or more ordinary subtags, or consists entirely of *.

Examples include:

de
de-DE
zh-Hant
*

Parse a basic range explicitly with ParseBasic():

var range = LanguageRangeParser.ParseBasic("de-DE");

For input that may be invalid:

if (LanguageRangeParser.TryParseBasic(value, out var range))
{
    Console.WriteLine(range);
}

Check syntax without creating a range:

LanguageRangeParser.IsBasic("de-DE"); // true

Basic ranges are required by RFC 4647 Basic Filtering and Lookup.

Extended language ranges

Extended ranges additionally permit wildcard subtags:

de-*-DE
*-Latn
zh-*-CN

Use Parse() for the extended grammar:

var range = LanguageRangeParser.Parse("de-*-DE");

The extended grammar includes all valid basic ranges, so this is also valid:

var range = LanguageRangeParser.Parse("de-DE");

Use TryParse() when invalid input is expected:

if (LanguageRangeParser.TryParse(value, out var range))
{
    Console.WriteLine(range);
}

Or test the grammar directly:

LanguageRangeParser.IsExtended("de-*-DE"); // true

Parse() versus ParseBasic()

Use Parse() when an extended range is acceptable:

var range = LanguageRangeParser.Parse("de-*-DE");

Use ParseBasic() when the range is intended for an operation that requires basic syntax:

var range = LanguageRangeParser.ParseBasic("de-DE");

This makes invalid combinations fail early rather than later when a matching operation is called.

Original and normalized values

Like LanguageTag, LanguageRange preserves the original input:

var range = LanguageRangeParser.Parse("DE-*-Ch");

Console.WriteLine(range.Value); // DE-*-Ch

ToString() returns the normalized representation. Language ranges are case-insensitive and normalized to lowercase:

Console.WriteLine(range); // de-*-ch

The normalized subtags are available through Subtags:

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

Basic-range detection

Every parsed range exposes IsBasic:

var basic = LanguageRangeParser.Parse("de-DE");
var extended = LanguageRangeParser.Parse("de-*-DE");

Console.WriteLine(basic.IsBasic);    // true
Console.WriteLine(extended.IsBasic); // false

This is useful when a range was parsed using the general Parse() method but later passed to an operation that only accepts basic ranges.

Wildcard ranges

The standalone wildcard is valid in both basic and extended syntax:

var range = LanguageRangeParser.ParseBasic("*");

Console.WriteLine(range.IsWildcard); // true

In filtering operations it matches all language tags. Lookup treats * differently because a wildcard does not identify one best language tag. See Language Tag Matching.

Registry-independent parsing

Language-range parsing is registry-independent. The parser validates the RFC 4647 range syntax but does not resolve subtags through the IANA Language Subtag Registry.

This is by design: matching language ranges is based on their subtag sequence rather than registry semantics.

See Language Tag Matching for Basic Filtering, Extended Filtering, and Lookup.

Clone this wiki locally