Architecture and Core Concepts
This guide explains why Pragmatic.Internationalization exists, how its pieces fit together, and how to choose the right abstraction for each situation. Read this before diving into the individual feature guides.
The Problem
Section titled “The Problem”.NET has built-in globalization support through CultureInfo, NumberFormatInfo, and resource files (.resx). But when you try to build a real multilingual application, the gaps emerge quickly.
Thread culture is fragile and global
Section titled “Thread culture is fragile and global”// Set culture for the current requestThread.CurrentThread.CurrentCulture = new CultureInfo("de-DE");
// Later, an async continuation runs on a different threadawait httpClient.GetAsync(url);
// Now Thread.CurrentThread.CurrentCulture might be en-US againvar price = total.ToString("C"); // Wrong culture!CultureInfo.CurrentCulture is thread-local. When the runtime schedules your continuation on a different thread pool thread, the culture resets. You can set CultureInfo.DefaultThreadCurrentCulture, but that is a process-wide setting that affects all requests — a multi-tenant disaster.
No concept of multi-scope culture
Section titled “No concept of multi-scope culture”A real application often needs different cultures simultaneously within the same request:
- UI culture: Italian (
it-IT) — for labels and messages shown to the user - Data culture: English (
en-US) — for API responses, database storage, and exports - Invoice culture: German (
de-DE) — for a specific business document
.NET gives you CurrentCulture and CurrentUICulture. Two slots. For anything more, you are on your own.
Resource files (.resx) do not compose
Section titled “Resource files (.resx) do not compose”<!-- Resources/Strings.en.resx --><data name="Welcome" xml:space="preserve"> <value>Welcome!</value></data>
<!-- Resources/Strings.it.resx --><data name="Welcome" xml:space="preserve"> <value>Benvenuto!</value></data>Resource files work, but they scatter translations across dozens of XML files with no way to validate completeness at build time. You can ship a release with half your Italian translations missing and not notice until a customer reports it. Plurals require manual if/else chains. And sharing translations across assemblies requires publishing satellite assemblies with specific folder structures.
Money is not a first-class concept
Section titled “Money is not a first-class concept”decimal price = 99.99m;string currency = "EUR";// No type safety: can accidentally add USD to EUR// No culture-aware formatting// No rounding rules per currency (JPY has 0 decimals, USD has 2)Every e-commerce application reinvents Money as a struct with an amount and a currency code. Without it, currency mismatches are silent bugs, rounding is inconsistent, and formatting varies by developer.
Plurals are language-specific and complex
Section titled “Plurals are language-specific and complex”// English: simplevar message = count == 1 ? "1 item" : $"{count} items";
// Russian: count % 10 == 1 && count % 100 != 11 -> singular// count % 10 in 2..4 && count % 100 not in 12..14 -> few// else -> many// Good luck maintaining that in every service.The CLDR defines six plural categories (Zero, One, Two, Few, Many, Other) with language-specific rules. Russian has three plural forms based on the last two digits. Arabic uses all six categories. A correct implementation requires a rule engine, not if/else.
The fundamental issue: .NET provides low-level building blocks, but leaves the developer to wire them together correctly for every application. Culture context management, multi-scope cultures, compile-time translation safety, type-safe money, and CLDR plurals are all recurring needs that should be solved once.
The Solution
Section titled “The Solution”Pragmatic.Internationalization provides a unified system with five pillars:
-
I18NContext: An
AsyncLocal-based ambient context that flows acrossawaitboundaries, supports multiple culture scopes (UI, Data, custom), and synchronizes with .NET thread cultures. -
Money and CurrencyCode: Value types that enforce same-currency arithmetic, provide ISO 4217 metadata, and format according to the ambient culture.
-
Formatting: Culture-aware extension methods and an injectable
GlobalizationFormatterfor numbers, dates, money, percentages, and file sizes. -
Translation Keys (SG): A source generator that reads JSON translation files at compile time and produces a static
Tclass with strongly-typedLocalizedStringproperties. Missing translations produce build warnings. -
Humanizers: Duration, ordinal, quantity, and relative time formatters with language-specific rules for 17+ languages.
The same “format a price for the current user” scenario:
// I18NContext is set by middleware (from Accept-Language, query string, or user preferences)// It flows across all await boundaries automatically
var price = Money.From(99.99m, CurrencyCode.EUR);var formatted = price.Format(); // "99,99 EUR" in it-IT, "$99.99" in en-US
// Or via DI-injectable formatterpublic class InvoiceService(GlobalizationFormatter formatter){ public string FormatTotal(Order order) => formatter.FormatMoney(order.Total);}No thread culture manipulation. No manual CultureInfo passing. The context is ambient, immutable, and thread-safe.
I18NContext: The Ambient Culture
Section titled “I18NContext: The Ambient Culture”I18NContext is the central coordination point. It stores the current culture using AsyncLocal<I18NContext?>, which means it flows automatically across await boundaries, into Task.Run, and through the entire async call chain.
How it works
Section titled “How it works”HTTP Request arrives | vI18NContextMiddleware resolves culture (query string -> Accept-Language -> provider chain -> default) | vI18NContext.SetFromConfig(resolvedConfig) - Sets AsyncLocal with immutable context - Syncs Thread.CurrentThread.CurrentUICulture - Syncs Thread.CurrentThread.CurrentCulture | vYour code reads I18N.Culture, I18N.UI, I18N.Data - Extension methods use I18NContext.EffectiveCulture - LocalizedString.Value uses I18NContext.Current.CultureCode - Money.Format() uses I18NContext.EffectiveCulture | vI18NContext.Clear() in middleware finally blockMulti-scope cultures
Section titled “Multi-scope cultures”The context stores three culture dimensions:
| Scope | Property | Purpose | Example |
|---|---|---|---|
| UI | I18N.UI | Display: labels, messages, user-facing content | it-IT |
| Data | I18N.Data | Storage: API responses, exports, database | en-US |
| Custom | I18N.Scope["invoicing"] | Specific needs: invoicing, reporting | de-DE |
// Set different cultures for different purposesI18NContext.SetCulture("it-IT"); // UI cultureI18NContext.SetDataCulture(CultureCode.FromString("en-US")); // Data cultureI18NContext.SetScope("invoicing", CultureCode.FromString("de-DE"));
// Access from anywhere in the async flowvar uiCulture = I18N.UI; // it-ITvar dataCulture = I18N.Data; // en-USvar invoiceCulture = I18N.Scope["invoicing"]; // de-DEWhen SyncScopes is enabled in I18NConfig, setting the UI culture automatically updates the Data culture to match. This is useful for simple applications that do not need separate cultures.
Strongly-typed scopes
Section titled “Strongly-typed scopes”Instead of magic strings, define scope types that implement ICultureScope:
public class InvoicingScope : ICultureScope{ public static string Name => "invoicing"; public static CultureCode DefaultCulture => CultureCode.Italian;}
// Set and get with type safetyI18N.SetScope<InvoicingScope>(CultureCode.German);var culture = I18N.GetScope<InvoicingScope>();
// Use with LocalizedStringvar title = product.Name.GetForScope<InvoicingScope>();Scoped execution
Section titled “Scoped execution”For temporary culture changes, use WithCulture / WithCultureAsync. These save the current context, execute the delegate, and restore the original context in a finally block:
// SynchronousI18NContext.WithCulture("de-DE", () =>{ var formatted = 1234.56m.FormatNumber(); // "1.234,56"});// Original culture restored here
// Asyncawait I18NContext.WithCultureAsync("fr-FR", async () =>{ await ProcessOrderAsync(); // Runs with fr-FR culture});
// With return valuevar result = I18NContext.WithCulture<string>("it-IT", () =>{ return T.Welcome.Value; // "Benvenuto nella nostra app!"});Fallback behavior
Section titled “Fallback behavior”If no context has been set explicitly, I18NContext.Current falls back to CultureInfo.CurrentCulture from the current thread. This means the system works out of the box in console applications and tests without any setup.
Money and CurrencyCode
Section titled “Money and CurrencyCode”The Money type
Section titled “The Money type”Money is a value type that pairs a decimal amount with a CurrencyCode. All arithmetic enforces same-currency operations at runtime:
var price = Money.From(99.99m, CurrencyCode.EUR);var tax = Money.From(7.50m, CurrencyCode.EUR);
var total = price + tax; // 107.49 EURvar discounted = total * 0.9m; // 96.74 EUR
// Currency mismatch throws InvalidOperationExceptionvar usd = Money.From(100m, CurrencyCode.USD);// var mixed = price + usd; // InvalidOperationException!Rounding respects the currency’s minor units (ISO 4217):
var price = Money.From(99.999m, CurrencyCode.USD);var rounded = price.RoundToMinorUnit(); // 100.00 (USD has 2 minor units)
var yen = Money.From(999.5m, CurrencyCode.JPY);var yenRounded = yen.RoundToMinorUnit(); // 1000 (JPY has 0 minor units)CurrencyCode
Section titled “CurrencyCode”All 180+ ISO 4217 currencies are available as static properties, source-generated from official data:
CurrencyCode.USD // US Dollar, symbol "$", 2 minor unitsCurrencyCode.EUR // Euro, symbol "euro", 2 minor unitsCurrencyCode.JPY // Japanese Yen, symbol "yen", 0 minor unitsEach currency exposes: Code, Name, Symbol, MinorUnits. Parsing and validation are provided via CurrencyCode.FromCode(), TryFromCode(), and IsValid().
Currency-aware formatting
Section titled “Currency-aware formatting”Money formatting is culture-aware through I18NContext:
I18NContext.SetCulture("en-US");total.Format(); // "$107.49"
I18NContext.SetCulture("de-DE");total.Format(); // "107,49 $"The I18N helper provides a convenience factory:
// Create money in the user's preferred currencyvar price = I18N.CreateMoney(99.99m); // Uses I18N.Currencyvar zero = I18N.Zero; // Zero in current currencyFormatting
Section titled “Formatting”Formatting is available in two forms: extension methods (for simple use) and GlobalizationFormatter (for DI-injectable, testable formatting).
Extension methods
Section titled “Extension methods”Extension methods read the ambient culture from I18NContext.EffectiveCulture:
// Numbers1234.56m.FormatNumber(); // "1,234.56" (en-US) / "1.234,56" (de-DE)1234.56m.FormatNumber(3); // With 3 decimal places0.156m.FormatPercent(); // "15.60%"1_500_000L.FormatFileSize(); // "1.4 MB"
// DatesDateTimeOffset.Now.FormatDate(); // "3/23/2026" (en-US) / "23.03.2026" (de-DE)DateTimeOffset.Now.FormatTime(); // "3:45 PM" / "15:45"DateTimeOffset.Now.FormatDateTime(); // "3/23/2026 3:45 PM"GlobalizationFormatter (DI-injectable)
Section titled “GlobalizationFormatter (DI-injectable)”For services that need testable formatting, inject GlobalizationFormatter:
public class InvoiceService(GlobalizationFormatter formatter){ public InvoiceDto Format(Invoice invoice) => new() { Total = formatter.FormatMoney(invoice.Total), TaxRate = formatter.FormatPercent(invoice.TaxRate), DueDate = formatter.FormatDate(invoice.DueDate), FileSize = formatter.FormatFileSize(invoice.AttachmentBytes) };}GlobalizationFormatter is registered as scoped by AddPragmaticInternationalization() and reads the culture from the current I18NContext at resolution time.
Available methods:
| Method | Description |
|---|---|
FormatMoney(money) | Money with currency symbol |
FormatMoney(amount, currency) | Amount + currency as Money |
FormatNumber(decimal, decimals) | Decimal with thousand separators |
FormatInteger(long) | Integer with thousand separators |
FormatPercent(decimal, decimals) | Decimal as percentage |
FormatFileSize(long) | Bytes as human-readable size |
FormatDate(DateTimeOffset) | Short date |
FormatDate(DateOnly) | Short date |
FormatTime(TimeOnly) | Short time |
FormatTime(DateTimeOffset) | Short time |
FormatDateTime(DateTimeOffset) | Short date + short time |
FormatDateTimeLong(DateTimeOffset) | Long date + long time |
Translation Keys (Source Generator)
Section titled “Translation Keys (Source Generator)”The Pragmatic.Internationalization.SourceGenerator reads JSON translation files at compile time and produces a static T class with LocalizedString properties. Translations are embedded in the assembly — no runtime file loading, no I/O, no startup cost.
How it works
Section titled “How it works”translations/en.json + translations/it.json | v [assembly: TranslationKeys] | v Source Generator | v Generated static T class with LocalizedString.From(("en", "..."), ("it", "..."))At build time, the SG:
- Finds all
AdditionalFilesmatching recognized translation folders - Auto-detects the folder structure (folder-per-culture, flat-with-suffix, or simple)
- Parses JSON files and merges translations by key and culture
- Generates a static
Tclass withLocalizedStringproperties - Emits diagnostics for parse errors (PRAG1800), duplicate keys (PRAG1801), and missing translations (PRAG1802)
LocalizedString vs T class
Section titled “LocalizedString vs T class”Most applications use both together. The choice depends on whether the content is static (developer-controlled) or dynamic (user-generated).
| 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 |
| Type Safety | Dictionary-like access (greeting["de"]) | Strongly-typed properties (T.Errors.NotFound) |
| Performance | Lazy loading from DB | Zero-cost (in-memory) |
| Fallback | Automatic culture chain via I18NContext | Same chain (returns LocalizedString) |
| EF Core | Value converter to JSON column | Not applicable (static class) |
Use LocalizedString for: product names, category descriptions, CMS content, invoice notes.
Use T class for: UI labels, error messages, email subjects, validation messages, system notifications.
// Hybrid usage -- both in the same handlervar product = await _repo.GetByIdAsync(Id, ct);if (product is null) return new NotFoundError(T.Errors.ProductNotFound); // T class (fixed)
return new ProductResponse{ Name = product.Name.Value, // LocalizedString (per-entity, from DB) Description = product.Description.Value};Folder structure auto-detection
Section titled “Folder structure auto-detection”The SG auto-detects three folder layouts from the first AdditionalFile path. No configuration needed.
Structure A: Folder per culture — each culture has its own subdirectory.
translations/+-- en/| +-- common.json| +-- errors.json+-- it/ +-- common.json +-- errors.jsonStructure B: Flat with culture suffix — culture code in the filename.
translations/+-- common.en.json+-- common.it.json+-- errors.en.json+-- errors.it.jsonStructure C: Simple — one file per culture, filename is the culture code.
translations/+-- en.json+-- it.jsonRecognized root folders: translations, i18n, locales, lang. Files matching *.translations.json or *.i18n.json are also included.
EmbedTranslations mode
Section titled “EmbedTranslations mode”When EmbedTranslations = true (default), the T class embeds all translation values at compile time:
// Generated: all values baked into the assemblypublic static LocalizedString Welcome => LocalizedString.From( ("en", "Welcome!"), ("it", "Benvenuto!"));When EmbedTranslations = false, the T class generates LocalizationKey references for runtime lookup:
// Generated: key only, resolved at runtime via ILocalizationProviderpublic static LocalizationKey Welcome => new("welcome");This allows swapping translation sources (database, remote API) without recompiling.
Plural Rules
Section titled “Plural Rules”Pragmatic implements CLDR-compliant plural rules for 40+ languages. The rules are organized by language family in partial classes: PluralRules.Germanic.cs, PluralRules.Romance.cs, PluralRules.Slavic.cs, PluralRules.Other.cs.
Six CLDR categories
Section titled “Six CLDR categories”| Category | Description | Example Languages |
|---|---|---|
Zero | Explicit zero form | Arabic, Welsh, Latvian |
One | Singular form | English, German, Russian, most languages |
Two | Dual form | Arabic, Welsh, Hebrew, Slovenian |
Few | Paucal / small number | Russian (2-4), Polish (2-4), Czech (2-4) |
Many | Large number form | Russian (5-20), Arabic (11-99) |
Other | Default plural | All languages (always present as fallback) |
Language family behavior
Section titled “Language family behavior”Germanic (en, de, nl, sv, da, no, fi): Simple one/other split. Count 1 is One, everything else is Other.
Romance (fr, it, es, pt, ca): Both 0 and 1 are One. Count 2+ is Other.
East Slavic (ru, uk, be): One/few/many based on last digits. 1 (not 11) is One. 2-4 (not 12-14) is Few. Everything else is Many.
East Asian (ja, zh, ko, vi, th) and Turkish-family (tr, az): No plural forms, always Other.
PluralRules.GetCategory("en", 1); // OnePluralRules.GetCategory("en", 5); // OtherPluralRules.GetCategory("fr", 0); // One (Romance: 0 is singular)PluralRules.GetCategory("ru", 2); // FewPluralRules.GetCategory("ru", 5); // ManyPluralRules.GetCategory("ar", 2); // Two (Arabic uses all 6 categories)PluralRules.GetCategory("ja", 1); // Other (no plural forms)Using plurals in translations
Section titled “Using plurals in translations”Define plural forms in JSON using CLDR category names as keys:
{ "items": { "one": "1 item", "other": "{count} items" }}Resolve via StringLocalizer:
var localizer = new StringLocalizer(provider, options);localizer.Plural("items", 1); // "1 item"localizer.Plural("items", 5); // "5 items"When a specific category is not defined, PluralString falls back to Other. If Other is also missing, it returns an empty string.
Humanizers
Section titled “Humanizers”Humanizers format values in a way that humans read naturally. Four formatters are provided.
DurationFormatter
Section titled “DurationFormatter”Formats TimeSpan values with language-specific units. Three formats are available:
| Format | Example (en) | Example (de) |
|---|---|---|
Short | 2h 30m | 2Std 30Min |
Long | 2 hours 30 minutes | 2 Stunden 30 Minuten |
Compact | 2:30:00 | 2:30:00 |
var formatter = new DurationFormatter("en", DurationFormat.Short);formatter.Format(TimeSpan.FromMinutes(150)); // "2h 30m"
var longFmt = DurationFormatter.Long("en");longFmt.Format(TimeSpan.FromHours(2.5)); // "2 hours 30 minutes"Supported languages: en, de, fr, it, es, pt, ru, zh, ja, ko, ar, nl, pl, sv, no, da, fi.
OrdinalFormatter
Section titled “OrdinalFormatter”Formats numbers as ordinals with language-specific suffixes:
var ordinal = new OrdinalFormatter("en");ordinal.Format(1); // "1st"ordinal.Format(2); // "2nd"ordinal.Format(21); // "21st"
var german = OrdinalFormatter.ForCulture("de");german.Format(1); // "1."
var feminine = new OrdinalFormatter("fr", OrdinalGender.Feminine);feminine.Format(1); // "1ere"QuantityFormatter
Section titled “QuantityFormatter”Abbreviates large numbers with language-specific suffixes:
var qty = new QuantityFormatter("en");qty.Format(1_500); // "1.5K"qty.Format(2_300_000); // "2.3M"qty.Format(1_200_000_000); // "1.2B"
var german = QuantityFormatter.ForCulture("de");german.Format(1_500_000); // "1,5 Mio"RelativeTimeFormatter
Section titled “RelativeTimeFormatter”Formats time spans as human-readable relative time:
var relTime = new RelativeTimeFormatter(localizer);relTime.Format(TimeSpan.FromMinutes(5)); // "5 minutes ago"relTime.Format(TimeSpan.FromDays(1)); // "yesterday"relTime.Format(TimeSpan.FromDays(14)); // "2 weeks ago"RelativeTimeFormatter requires an IStringLocalizer because the labels (“ago”, “yesterday”, “weeks”) are themselves translatable.
Localization Providers
Section titled “Localization Providers”For runtime-based localization (when EmbedTranslations = false or for supplemental translations), the provider system offers a composable pipeline.
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”| Provider | Description | Use Case |
|---|---|---|
InMemoryLocalizationProvider | Programmatic, fluent API | Development, testing |
JsonLocalizationProvider | Reads JSON files at runtime | File-based translations |
CompositeLocalizationProvider | Chains providers by priority | Multi-source fallback |
var provider = new InMemoryLocalizationProvider() .AddString("en", "welcome", "Welcome!") .AddString("de", "welcome", "Willkommen!") .AddPlural("en", "items", (PluralCategory.One, "1 item"), (PluralCategory.Other, "{count} items"));
var composite = new CompositeLocalizationProvider( new[] { jsonProvider, memoryProvider });// Checked in Priority order (descending). First non-null 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 switchASP.NET Core Integration
Section titled “ASP.NET Core Integration”The Pragmatic.Internationalization.AspNetCore package provides middleware, DI registration, and JSON serialization.
builder.Services.AddPragmaticInternationalization(options =>{ options.DefaultUICulture = CultureCode.EnglishUS; options.SupportedCultures = [CultureCode.English, CultureCode.Italian, CultureCode.German]; options.SyncScopes = true;});
var app = builder.Build();app.UsePragmaticInternationalization();Culture resolution order
Section titled “Culture resolution order”The middleware resolves culture in this order:
- Query string:
?culture=de-DE - Accept-Language header: standard HTTP header
- Provider chain:
SystemConfigProvider(fromappsettings.json, priority 0), custom providers at higher priorities - Default culture: from
I18NOptions.DefaultUICulture
Provider chain
Section titled “Provider chain”The config provider system (II18NConfigProvider) allows layered culture resolution:
| Provider | Priority | Source |
|---|---|---|
RequestConfigProvider | 300 | Query string, Accept-Language |
UserConfigProvider | 200 | User preferences from database |
TenantConfigProvider | 100 | Tenant settings |
SystemConfigProvider | 0 | appsettings.json |
Register additional providers via the builder:
builder.Services.AddPragmaticInternationalization(options => { ... }) .AddConfigProvider<TenantConfigProvider>() .AddConfigProvider<UserConfigProvider>();JSON serialization
Section titled “JSON serialization”Money and CurrencyCode types serialize automatically when AddPragmaticInternationalization() is called:
{ "price": { "amount": 99.99, "currency": "EUR" }}JSON converters are registered for: Money, CurrencyCode, CultureCode, LanguageCode, CountryCode.
EF Core Integration
Section titled “EF Core Integration”The Pragmatic.Internationalization.EFCore package provides value converters for database storage.
// In DbContext.OnModelCreatingmodelBuilder.ApplyPragmaticInternationalization();
// Or via conventionsprotected override void ConfigureConventions(ModelConfigurationBuilder builder){ builder.ApplyPragmaticInternationalizationConventions();}Value converters:
CurrencyCodemaps tovarchar(3)Moneyis configured with separate amount and currency columnsLocalizedStringmaps to a JSON column
Ecosystem Integration
Section titled “Ecosystem Integration”Pragmatic.Internationalization integrates with other Pragmatic modules through the ambient I18NContext.
Persistence
Section titled “Persistence”Entities with LocalizedString properties store translations as JSON columns via EF Core value converters. The Money type is stored as two columns (amount + currency code).
Endpoints
Section titled “Endpoints”Endpoint handlers can access formatted values via extension methods or inject GlobalizationFormatter. The middleware sets I18NContext before any endpoint handler runs.
Validation
Section titled “Validation”The AspNetCore package provides validation attributes: [NonNegativeMoney], [PositiveMoney], [SupportedCurrency]. These validate Money and CurrencyCode properties on endpoint inputs.
Composition
Section titled “Composition”When Pragmatic.Composition is referenced, the SG emits assembly-level [PragmaticMetadata] attributes containing translation metadata (class name, namespace, key count, cultures, files). This enables host applications to discover translation metadata from referenced assemblies.
Testing
Section titled “Testing”TestI18N and TestI18NScope
Section titled “TestI18N and TestI18NScope”The Testing namespace provides helpers for isolated test execution:
// Static helper -- sets culture, runs action, restores previous contextTestI18N.WithCulture(CultureCode.Italian, () =>{ var formatted = price.Format(); formatted.Should().Contain(",");});
// Disposable scope -- for more controlusing var scope = new TestI18NScope(CultureCode.German);var result = 1234.56m.FormatNumber(); // "1.234,56"// Context restored when scope is disposed
// Separate UI and Data culturesTestI18N.WithCultures( uiCulture: CultureCode.Italian, dataCulture: CultureCode.English, () => { I18N.UI.Code.Should().Be("it"); I18N.Data.Code.Should().Be("en"); });Manual capture/restore
Section titled “Manual capture/restore”For frameworks that do not support using scopes easily:
var snapshot = I18NContext.Capture();try{ I18NContext.SetCulture("de-DE"); // ... test code ...}finally{ I18NContext.Restore(snapshot);}See Also
Section titled “See Also”- Getting Started — Install and configure in 5 minutes
- Translation Keys —
[TranslationKeys], SG-generated constants, folder structures - Common Mistakes — Wrong/right patterns with explanations
- Troubleshooting — Diagnostics reference, FAQ, problem/solution guide