Skip to content

Translation Keys

The Pragmatic.Internationalization.SourceGenerator creates a static T class with LocalizedString properties from JSON translation files. Translations are embedded at compile-time — no runtime file loading required.

Create JSON translation files in your project:

translations/
+-- en.json
+-- it.json
en.json
{
"welcome": "Welcome to our app!",
"errors": {
"notFound": "Resource not found"
}
}
// it.json
{
"welcome": "Benvenuto nella nostra app!",
"errors": {
"notFound": "Risorsa non trovata"
}
}

In your .csproj:

<ItemGroup>
<AdditionalFiles Include="translations/*.json" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Pragmatic.Internationalization.SourceGenerator\..."
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>

In AssemblyAttributes.cs (or any file):

using Pragmatic.Internationalization.Attributes;
[assembly: TranslationKeys]

From the JSON files above, the generator produces:

public static class T
{
public static LocalizedString Welcome => LocalizedString.From(
("en", "Welcome to our app!"),
("it", "Benvenuto nella nostra app!"));
public static class Errors
{
public static LocalizedString NotFound => LocalizedString.From(
("en", "Resource not found"),
("it", "Risorsa non trovata"));
}
}
// Current culture access (via I18NContext)
I18NContext.SetCulture("en");
Console.WriteLine(T.Welcome.Value); // "Welcome to our app!"
I18NContext.SetCulture("it");
Console.WriteLine(T.Welcome.Value); // "Benvenuto nella nostra app!"
// Direct culture access via indexer
Console.WriteLine(T.Welcome["en"]); // "Welcome to our app!"
Console.WriteLine(T.Welcome["it"]); // "Benvenuto nella nostra app!"
// Implicit string conversion (uses current culture)
string message = T.Welcome;
// LocalizedString properties
var cultures = T.Welcome.Cultures; // ["en", "it"]
var count = T.Welcome.Count; // 2
var isEmpty = T.Welcome.IsEmpty; // false
[assembly: TranslationKeys(
ClassName = "T", // Default: "T"
Namespace = "", // Default: auto-derived from folder
EmbedTranslations = true, // Default: true (LocalizedString with values)
ByFile = true, // Default: true (nested classes per file)
DefaultCulture = "en")] // Default: "en"
PropertyDefaultDescription
ClassName"T"Root class name for generated keys
Namespace""Namespace (auto-derived if empty)
EmbedTranslationstruetrue: LocalizedString.From(...) with values. false: LocalizationKey for runtime lookup
ByFiletruetrue: nested classes per file. false: all keys at root level
DefaultCulture"en"Default culture for ordering translations

When EmbedTranslations = false, the T class generates LocalizationKey references instead of LocalizedString. These are lightweight key strings resolved at runtime through an ILocalizationProvider. This allows swapping translation sources without recompiling.

The source generator auto-detects three folder layouts from the first AdditionalFile path. No configuration needed.

Detection order:

  1. Is the parent directory a culture code? —> Folder per culture
  2. Does the filename contain a dot where the last segment is a culture code? —> Flat with suffix
  3. Otherwise —> Simple (filename is the culture code)
translations/
+-- en/
| +-- common.json
| +-- errors.json
+-- it/
+-- common.json
+-- errors.json
<AdditionalFiles Include="translations/**/*.json" />

Each culture has its own subdirectory. Files with the same name across cultures are matched together. Each file becomes a nested class: T.Common.Welcome, T.Errors.NotFound.

translations/
+-- common.en.json
+-- common.it.json
+-- errors.en.json
+-- errors.it.json
<AdditionalFiles Include="translations/*.json" />

All files in one folder. The culture code is the last dot-segment before .json. Same result: T.Common.Welcome, T.Errors.NotFound.

Structure C: Simple (One File per Culture)

Section titled “Structure C: Simple (One File per Culture)”
translations/
+-- en.json
+-- it.json
<AdditionalFiles Include="translations/*.json" />

One file per culture. Simplest layout. Dotted JSON keys create nested classes: "errors.notFound" —> T.Errors.NotFound.

Files are picked up under these directory names: translations, i18n, locales, lang. Files matching *.translations.json or *.i18n.json are also included regardless of folder.

For runtime-based localization (when EmbedTranslations = false or for supplemental translations):

public interface ILocalizationProvider
{
IReadOnlyList<string> SupportedCultures { get; }
int Priority => 0; // Higher = checked first
string? GetString(string key, string culture);
PluralString? GetPlural(string key, string culture);
IReadOnlyDictionary<string, string> GetAll(string culture);
IReadOnlyDictionary<string, PluralString> GetAllPlurals(string culture);
}

InMemoryLocalizationProvider — programmatic, for development and testing:

var provider = new InMemoryLocalizationProvider()
.AddString("en", "welcome", "Welcome!")
.AddString("de", "welcome", "Willkommen!")
.AddPlural("en", "items",
(PluralCategory.One, "1 item"),
(PluralCategory.Other, "{count} items"));

JsonLocalizationProvider — reads from JSON files at runtime:

var jsonProvider = new JsonLocalizationProvider("Resources/Translations", options);

CompositeLocalizationProvider — chains multiple providers with priority-based fallback:

var composite = new CompositeLocalizationProvider(
new[] { jsonProvider, memoryProvider });

Providers are checked in order of Priority (descending). The first provider to return a non-null value wins.

The main interface for string localization with interpolation and pluralization:

var localizer = new StringLocalizer(provider, options);
var welcome = localizer["welcome"]; // Simple lookup
var greeting = localizer["hello", "John"]; // Interpolation
var items = localizer.Plural("items", 5); // Plural
var german = localizer.WithCulture("de"); // Culture switch
AspectLocalizedStringT Class
StorageDatabase (JSON column)Assembly (embedded at compile-time)
ScopePer-entity instanceApplication-wide
ContentDynamic, user-generatedFixed, developer-controlled
MutabilityAdd/remove translations at runtimeRecompile to change
PerformanceLazy loading from DBZero-cost (in-memory)

Use LocalizedString for database-driven per-entity content: product names, descriptions, CMS content.

Use T class for compile-time safe application strings: UI labels, error messages, validation messages.

Most applications use both together:

// T class for fixed application strings
return new NotFoundError(T.Errors.ProductNotFound);
// LocalizedString for dynamic entity data
var productName = product.Name.Value;

CLDR-compliant plural rules for 40+ languages. Six categories: Zero, One, Two, Few, Many, Other.

PluralRules.GetCategory("en", 1); // One
PluralRules.GetCategory("en", 5); // Other
PluralRules.GetCategory("ru", 2); // Few
PluralRules.GetCategory("ar", 0); // Zero

Plural rules are organized by language family: Germanic, Romance, Slavic, Other. The method extracts the base language code (e.g., "de-AT" —> "de") before applying rules.

Define plural forms in JSON:

{
"items": {
"one": "1 item",
"other": "{count} items"
}
}

When a specific category is not defined, PluralString falls back to Other. If Other is also missing, it returns an empty string.

When Pragmatic.Composition is referenced, the generator emits assembly metadata:

[assembly: PragmaticMetadata(MetadataCategory.Translations, "1.0.0", """
{
"className": "T",
"namespace": "MyApp",
"totalKeys": 25,
"cultures": ["en", "it", "de"],
"files": ["common", "errors", "validation"]
}
""")]

This enables host applications to discover translation metadata from referenced assemblies.

IDSeverityDescription
PRAG1800ErrorTranslation JSON file could not be parsed
PRAG1801WarningDuplicate translation key detected for same culture
PRAG1802WarningTranslation key exists in default culture but missing in another
PRAG1803InfoTranslation file contains no valid translation keys