SkillAgentSearch skills...

Po

A .NET library for reading and writing Gettext PO files.

Install / Use

/learn @adams85/Po
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Karambolo.PO

This class library enables parsing, building and generating GetText PO files on the .NET platform. (Supported framework versions: .NET Framework 4+, .NET Standard 1.0+, .NET Core 1.0+, .NET 5+).

NuGet Release Donate

The implementation is based on the PO file format specification of the GNU gettext utilities documentation. All the parts relevant to .NET programming is covered including full support for

  • contexts for identical text disambiguation,
  • plural forms (including plural form selector expressions),
  • metadata comments,
  • proper formatting of long texts.

Where the documentation was not specific enough, compatibility with Poedit took precedence.

Only synchronous API is available, async I/O is not supported for the moment.

Editions

As of version 1.3 three editions (builds) of the library are available with different feature sets:

| Edition | NuGet Package ID | Missing Features | Dependencies | |--|--|--|--| | Full | Karambolo.PO | | none for .NET 9+, Karambolo.Common for older .NET versions | | Compact | Karambolo.PO.Compact | <ul><li>PreserveHeadersOrder option (see below)</li></ul> | | | Minimal | Karambolo.PO.Minimal | <ul><li>PreserveHeadersOrder option (see below)</li><li>Plural expression parsing and evaluation</li></ul> |

Minimal is the most lightweight edition. You may choose it if you don't need to lookup plural form translations in your application.

Compact provides almost all the features the Full package does but doesn't require any 3rd party dependencies for any .NET version.

  • If your project targets .NET 9+, just choose the Full edition.
  • Otherwise, the Compact edition might be a better choice if your project doesn't need the PreserveHeadersOrder feature and/or the Karambolo.Common library.

Code samples

Parsing PO content

var parser = new POParser(new POParserSettings
{
    // parser options...
});

TextReader reader = ...;
var result = parser.Parse(reader);

if (result.Success)
{
    var catalog = result.Catalog;
    // process the parsed data...
}
else
{
    var diagnostics = result.Diagnostics;
    // examine diagnostics, display an error, etc...
}
Remarks:
  • As of version 1.1.0, convenience overloads accepting String or Stream are available for POParser.Parse method, as well.
  • The parser instance can be re-used but it's not safe to use it concurrently from multiple threads.
Options:

| | Description | Default value | |---|---|---| | PreserveHeadersOrder | Retain the order of metadata headers. POCatalog.Headers property will be set to a dictionary instance which preserves insertion order. (Available in the Full build only.) | false | | ReadHeaderOnly | Parse only the metadata header item. | false | | SkipInfoHeaders | Parse only the relevant metadata headers (Content-Transfer-Encoding, Content-Type, Language and Plural-Forms) and ignore the rest. | false | | SkipComments | Parse no comments at all, not even the ones containing metadata. | false | | StringDecodingOptions. KeepKeyStringsPlatformIndependent | Keeps msgctxt, msgid and msgid_plural strings platform-independent: preserves \n escape sequences in key strings, that is, prevents them from being replaced with Environment.NewLine. (Available only since version 1.7.0) | false | | StringDecodingOptions. KeepTranslationStringsPlatformIndependent | Keeps msgstr strings platform-independent: preserves \n escape sequences in translation strings, that is, prevents them from being replaced with Environment.NewLine. (Available only since version 1.7.0) | false |

Generating PO file content

POCatalog catalog = ...;

var generator = new POGenerator(new POGeneratorSettings {
{
    // generator options...
});

TextWriter writer = ...;
generator.Generate(writer, catalog);
writer.Flush();
Remarks:
  • As of version 1.1.0, convenience overloads accepting StringBuilder or Stream are available for POGenerator.Generate method, as well.
  • The generator instance can be re-used but it's not safe to use it concurrently from multiple threads.
Options:

| | Description | Default value | |---|---|---| | IgnoreEncoding | Don't check whether the text encoding of the writer and the text encoding set for the catalog match. | false | | IgnoreLineBreaks | Don't respect line breaks ("\n") when wrapping texts. | false | | IgnoreLongLines | Don't wrap long lines (lines longer than 80 characters). | false | | MaxLineLength | The maximum number of characters (more precisely, Unicode code points) allowed per line. Applies only when IgnoreLongLines is false. (Available only since version 1.13.0) | 80 | | PreserveHeadersOrder | Don't sort but retain the order of metadata headers. POCatalog.Headers property should be set to a dictionary instance which preserves insertion order. (Available in the Full build only.) | false | | SkipInfoHeaders | Generate only the relevant metadata headers (Content-Transfer-Encoding, Content-Type, Language and Plural-Forms) and ignore the rest. | false | | SkipComments | Generate no comments. | false |

Building PO catalogs by code

var catalog = new POCatalog();

// setting comments for the header item
catalog.HeaderComments = new POComment[]
{
    new POTranslatorComment { Text = "Some header comment" }
};

// setting required headers
catalog.Encoding = "UTF-8";
catalog.PluralFormCount = 2;
catalog.PluralFormSelector = "(n != 1)";
catalog.Language = "en_US";

// setting custom headers
catalog.Headers = new Dictionary<string, string>
{
    { "POT-Creation-Date", "2018-08-01 12:34+0000" },
    { "Project-Id-Version", "Some Awesome App 1.0" },
    { "X-Generator", "My Awesome PO Generator Tool 1.0" },
};

// adding a plural entry with text context and all kinds of comments
var key = new POKey("{0} user", "{0} users", "/Views/Home/Index");
IPOEntry entry = new POPluralEntry(key)
{
    "Translation of {0} user",
    "Translation of {0} users",
};
entry.Comments = new POComment[]
{
    new POTranslatorComment { Text = "Some translator comment" },
    new POExtractedComment { Text = "Some extracted comment" },
    new POReferenceComment { References = new POSourceReference[] { new POSourceReference("/Views/Home/Index.cshtml", 8) } },
    new POFlagsComment { Flags = new HashSet<string> { "fuzzy" } },
    new POPreviousValueComment { IdKind = POIdKind.Id, Value = "{0} user logged in." },
    new POPreviousValueComment { IdKind = POIdKind.PluralId, Value = "{0} users logged in." },
};
catalog.Add(entry);

// adding a singular entry with multi-line text
key = new POKey($"Multi-line{Environment.NewLine}text.");
entry = new POSingularEntry(key)
{
    Translation = $"Translation of multi-line{Environment.NewLine}text."
};
catalog.Add(entry);

Retrieving translations from PO catalogs

// querying translation
var key = new POKey($"Multi-line{Environment.NewLine}text.");
var translation = catalog.GetTranslation(key);

// querying translation for the count of 5
key = new POKey("{0} user", "{0} users", "/Views/Home/Index");
translation.GetTranslation(key, 5);
Remarks:
  • POCatalog.GetTranslation(POKey key) returns the default translation: msgstr for a singular entry and msgstr[0] for a plural entry, respectively.
  • POCatalog.GetTranslation(POKey key, int count) returns the plural form translation corresponding the count argument. In the case of singular entries count is ignored and msgstr is returned always. In the case of plural entries, count is mapped to the corresponding index using the POCatalog.PluralFormSelector expression and msgstr[index] is returned. (The index is adjusted to the actual count of translations so no exception is thrown even if the index were out of bounds.)
  • When using the Minimal build, plural form selection is not available and thus the first translation is always returned even for plural entries.

Real-world application

Implementing multi-language support in software is usually a tedious, time-consuming task, so the capabilities of the localization infrastructure (API, tooling, etc.) may have a significant impact on development experience and, thus, the time required for development. The out-of-the-box localization infrastructure provided by .NET is based on XML resources and satellite assemblies. It gets the job done but is not as convenient as it could be.

PO-based localization, besides providing some additional features over .NET resources, is an excellent alternative as there are great and mature tools around which can aid and speed up the process of translating text resources and keeping them in sync.

However, there are some pieces missing to use PO-based localization in .NET applications. This library was created with an intention to have a base on which these pieces can be built.

The repository also includes an ASP.NET Core web application project which demonstrates how to complete the puzzle to get a complete and efficient localization solution based on PO files. (The idea is not tied to ASP.NET Core at all, it can be applied to other types of .NET projects easily.)

The demo project is based on the default web application template which ships with .NET (it was created by issuing the command dotnet new webapp --razor-runtime-compilation), so by examining [this diff](https://github.com/adams85/po/commit/54f20e1

View on GitHub
GitHub Stars79
CategoryContent
Updated10d ago
Forks16

Languages

C#

Security Score

95/100

Audited on Mar 22, 2026

No findings