Skip to content

Building Language Tags

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

LanguageTagBuilder provides a fluent API for constructing BCP 47 language tags from individual components.

The builder validates each component according to its own RFC 5646 grammar and delegates final structural validation and normalization to Bcp47Parser when Build() is called.

Basic construction

A simple language-region tag can be constructed as follows:

var tag = new LanguageTagBuilder()
    .Language("de")
    .Region("DE")
    .Build();

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

Script and region can be combined:

var tag = new LanguageTagBuilder()
    .Language("zh")
    .Script("Hans")
    .Region("CN")
    .Build();

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

The builder methods return the same builder instance, so calls can be chained.

Language

Set the primary language using Language():

var builder = new LanguageTagBuilder()
    .Language("en");

A primary language subtag must contain between 2 and 8 ASCII letters.

The builder validates the component immediately. A value that belongs to another component grammar is not silently reinterpreted later:

new LanguageTagBuilder()
    .Language("i"); // throws ArgumentException

Extended-language subtags

Add an extlang using Extlang():

var tag = new LanguageTagBuilder()
    .Language("zh")
    .Extlang("cmn")
    .Build();

An extlang subtag must contain exactly three ASCII letters.

Final structural validation still applies when Build() is called. In particular, the library rejects multiple extlang subtags as structurally invalid.

Script

Set the script with Script():

var tag = new LanguageTagBuilder()
    .Language("zh")
    .Script("Hans")
    .Build();

A script subtag must contain exactly four ASCII letters.

This is rejected immediately:

new LanguageTagBuilder()
    .Language("de")
    .Script("US");

US has region syntax, not script syntax.

Region

Set the region with Region():

var tag = new LanguageTagBuilder()
    .Language("de")
    .Region("DE")
    .Build();

A region is either:

  • two ASCII letters, or
  • three ASCII digits

For example:

var tag = new LanguageTagBuilder()
    .Language("es")
    .Region("419")
    .Build();

A value such as 1901 is rejected by Region() because it has variant syntax instead.

Variants

Add variants with Variant():

var tag = new LanguageTagBuilder()
    .Language("sl")
    .Variant("rozaj")
    .Variant("biske")
    .Build();

Console.WriteLine(tag); // sl-rozaj-biske

Variants are appended in the order in which they are added.

Duplicate variants are detected by the final structural validation:

var builder = new LanguageTagBuilder()
    .Language("sl")
    .Variant("rozaj")
    .Variant("ROZAJ");

builder.Build(); // throws FormatException

Extensions

Add an extension sequence using Extension():

var tag = new LanguageTagBuilder()
    .Language("en")
    .Extension('u', "ca", "gregory")
    .Build();

Console.WriteLine(tag); // en-u-ca-gregory

The singleton must be an ASCII alphanumeric character other than x or X. The x singleton is reserved for private use.

Each extension subtag must contain between 2 and 8 ASCII alphanumeric characters.

Extension sequences are normalized into deterministic singleton order by the parser:

var tag = new LanguageTagBuilder()
    .Language("en")
    .Extension('b', "bbb")
    .Extension('a', "aaa")
    .Build();

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

Duplicate extension singletons are rejected by Build().

Private use

Private-use subtags can follow an ordinary language tag:

var tag = new LanguageTagBuilder()
    .Language("de")
    .Region("DE")
    .PrivateUse("company", "internal")
    .Build();

Console.WriteLine(tag); // de-DE-x-company-internal

A private-use subtag contains between 1 and 8 ASCII alphanumeric characters.

Private-use-only tags

A builder without a primary language can construct a private-use-only tag:

var tag = new LanguageTagBuilder()
    .PrivateUse("company", "internal")
    .Build();

Console.WriteLine(tag);                  // x-company-internal
Console.WriteLine(tag.IsPrivateUseOnly); // true

Without a primary language, no extlang, script, region, variant, or extension components may be present.

For example, this is invalid:

var builder = new LanguageTagBuilder()
    .Script("Latn")
    .PrivateUse("example");

builder.Build(); // throws InvalidOperationException

Building from an existing tag

Use LanguageTagBuilder.From() to create a builder from an existing parsed tag:

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

var modified = LanguageTagBuilder
    .From(original)
    .Region("AT")
    .Build();

Console.WriteLine(modified); // de-AT

All decomposable components are copied, including extlangs, variants, extensions, and private-use subtags.

Private-use-only tags can also be copied:

var original = LanguageTag.Parse("x-company-internal");
var copy = LanguageTagBuilder.From(original).Build();

Grandfathered tags cannot be decomposed into builder components, so From() rejects them:

var tag = LanguageTag.Parse("i-klingon");
LanguageTagBuilder.From(tag); // throws ArgumentException

Builder validation and IANA validation

LanguageTagBuilder validates RFC 5646 component grammar and final registry-independent structure. It does not check whether a component is registered in IANA.

For example, building and registry validation are separate operations:

var tag = new LanguageTagBuilder()
    .Language("de")
    .Script("Abcd")
    .Build();

If registry membership matters, install Enbrea.Bcp47.Iana and validate the result afterward:

using Enbrea.Bcp47.Iana;

bool valid = tag.IsValidAgainstIanaRegistry();

See IANA Validation.

Clone this wiki locally