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.
1. Add Translation Files
Section titled “1. Add Translation Files”Create JSON translation files in your project:
translations/+-- en.json+-- it.json{ "welcome": "Welcome to our app!", "errors": { "notFound": "Resource not found" }}
// it.json{ "welcome": "Benvenuto nella nostra app!", "errors": { "notFound": "Risorsa non trovata" }}2. Include as AdditionalFiles
Section titled “2. Include as AdditionalFiles”In your .csproj:
<ItemGroup> <AdditionalFiles Include="translations/*.json" /></ItemGroup>3. Reference the Source Generator
Section titled “3. Reference the Source Generator”<ItemGroup> <ProjectReference Include="..\Pragmatic.Internationalization.SourceGenerator\..." OutputItemType="Analyzer" ReferenceOutputAssembly="false" /></ItemGroup>4. Add the Assembly Attribute
Section titled “4. Add the Assembly Attribute”In AssemblyAttributes.cs (or any file):
using Pragmatic.Internationalization.Attributes;
[assembly: TranslationKeys]Generated T Class
Section titled “Generated T Class”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 indexerConsole.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 propertiesvar cultures = T.Welcome.Cultures; // ["en", "it"]var count = T.Welcome.Count; // 2var isEmpty = T.Welcome.IsEmpty; // falseTranslationKeysAttribute Configuration
Section titled “TranslationKeysAttribute Configuration”[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"| Property | Default | Description |
|---|---|---|
ClassName | "T" | Root class name for generated keys |
Namespace | "" | Namespace (auto-derived if empty) |
EmbedTranslations | true | true: LocalizedString.From(...) with values. false: LocalizationKey for runtime lookup |
ByFile | true | true: nested classes per file. false: all keys at root level |
DefaultCulture | "en" | Default culture for ordering translations |
EmbedTranslations = false
Section titled “EmbedTranslations = false”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.
Folder Structures
Section titled “Folder Structures”The source generator auto-detects three folder layouts from the first AdditionalFile path. No configuration needed.
Detection order:
- Is the parent directory a culture code? —> Folder per culture
- Does the filename contain a dot where the last segment is a culture code? —> Flat with suffix
- Otherwise —> Simple (filename is the culture code)
Structure A: Folder per Culture
Section titled “Structure A: Folder per Culture”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.
Structure B: Flat with Culture Suffix
Section titled “Structure B: Flat with Culture Suffix”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.
Recognized Root Folders
Section titled “Recognized Root Folders”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.
Localization Providers
Section titled “Localization Providers”For runtime-based localization (when EmbedTranslations = false or for supplemental translations):
ILocalizationProvider
Section titled “ILocalizationProvider”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);}Built-In Providers
Section titled “Built-In Providers”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.
IStringLocalizer
Section titled “IStringLocalizer”The main interface for string localization with interpolation and pluralization:
var localizer = new StringLocalizer(provider, options);
var welcome = localizer["welcome"]; // Simple lookupvar greeting = localizer["hello", "John"]; // Interpolationvar items = localizer.Plural("items", 5); // Pluralvar german = localizer.WithCulture("de"); // Culture switchLocalizedString vs T Class
Section titled “LocalizedString vs T Class”| Aspect | LocalizedString | T Class |
|---|---|---|
| Storage | Database (JSON column) | Assembly (embedded at compile-time) |
| Scope | Per-entity instance | Application-wide |
| Content | Dynamic, user-generated | Fixed, developer-controlled |
| Mutability | Add/remove translations at runtime | Recompile to change |
| Performance | Lazy loading from DB | Zero-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 stringsreturn new NotFoundError(T.Errors.ProductNotFound);
// LocalizedString for dynamic entity datavar productName = product.Name.Value;Plural Rules
Section titled “Plural Rules”CLDR-compliant plural rules for 40+ languages. Six categories: Zero, One, Two, Few, Many, Other.
PluralRules.GetCategory("en", 1); // OnePluralRules.GetCategory("en", 5); // OtherPluralRules.GetCategory("ru", 2); // FewPluralRules.GetCategory("ar", 0); // ZeroPlural 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.
Metadata for Cross-Assembly Discovery
Section titled “Metadata for Cross-Assembly Discovery”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.
Diagnostics
Section titled “Diagnostics”| ID | Severity | Description |
|---|---|---|
| PRAG1800 | Error | Translation JSON file could not be parsed |
| PRAG1801 | Warning | Duplicate translation key detected for same culture |
| PRAG1802 | Warning | Translation key exists in default culture but missing in another |
| PRAG1803 | Info | Translation file contains no valid translation keys |