Skip to content

JSON Templates

This guide covers how to create, organize, and load JSON template files for localization.

Template Structure

Each JSON file is a flat key-value dictionary where keys are fully-qualified type names and values are format strings:

{
"Namespace.ClassName": "Template with {PropertyName} placeholders"
}

Keys must match the Type.FullName of the localizable type exactly.

Placeholder Syntax

Placeholders use the {PropertyName} syntax and are replaced with the corresponding public property values from the localizable type:

public class TransferError(string from, string to, decimal amount) : ILocalizable
{
public string From { get; } = from;
public string To { get; } = to;
public decimal Amount { get; } = amount;
}
{
"MyApp.Errors.TransferError": "Cannot transfer {Amount} from '{From}' to '{To}'"
}

Generic Types

The library uses Type.FullName as the template key. For generic types, .NET produces verbose, assembly-qualified names that include backtick notation and full type argument details:

MyApp.GenericResult`1[[System.String, System.Private.CoreLib, Version=10.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]

This means using generic types as JSON template keys is technically possible but impractical — the key is long, brittle (tied to runtime version), and hard to maintain.

InMemory Registry (Transparent)

InMemoryLocalizationRegistryBuilder.Add<T>() handles generic types transparently because both registration and lookup use the same Type.FullName:

// Both sides resolve the same FQDN key automatically
builder.Add<Result<OrderError>>("Order failed: {Message} (code {Code})");

For JSON-based localization, generic types require the exact Type.FullName string as the key, which is unwieldy and fragile. Instead, prefer one of these alternatives:

  • Use concrete (non-generic) wrapper types — e.g. OrderResult instead of Result<Order>
  • Implement ILocalizationProvider on the type itself (self-provider) for full control over localization
  • Register an ILocalizationProvider<T> via DI for external formatting logic

See the Custom Providers guide for implementation details on both provider approaches

JSONC Support

Template files support JSONC (JSON with Comments), so you can annotate templates by module, domain, or translation context without affecting parsing.

Supported Features

FeatureSyntaxExample
Single-line comments// comment// Orders Module
Block comments/* comment *//* Shared validation */
Inline commentsafter a value"key": "value", // note
Trailing commaslast item in object"key": "value",

Multi-Module Example

A single JSONC file can use comments as section headers to keep related templates grouped:

{
// -- Orders Module --
"MyApp.Orders.OrderStatus": "Order #{OrderNumber} is {Status}",
"MyApp.Orders.OrderShippedNotification": "Your order #{OrderNumber} has been shipped to {Address}",
// -- Payments Module --
"MyApp.Payments.PaymentDeclined": "Payment of {Amount} was declined: {Reason}",
"MyApp.Payments.RefundIssued": "A refund of {Amount} has been issued to {Method}",
/*
* -- Validation Errors --
* Shared across modules. Used in ProblemDetails responses.
*/
"MyApp.Validation.RequiredFieldError": "'{FieldName}' is required",
"MyApp.Validation.MaxLengthError": "'{FieldName}' must not exceed {MaxLength} characters",
"MyApp.Validation.InvalidEmailError": "'{FieldName}' is not a valid email address",
}

Organizing Templates

The simplest approach: keep all templates in one JSONC file and use comments as section dividers (as shown above). This works well for small-to-medium projects where a single file stays manageable.

Combining Multiple Sources

You can mix embedded resources and filesystem files in the same registration. Templates are merged with last-loaded-wins semantics: if the same key appears in multiple sources, the value from the last builder call takes precedence.

Override Layering Example

A common pattern is to ship base templates inside a shared library assembly and let the host application override specific keys at runtime:

services.AddLocalization(builder =>
{
// Layer 1: base templates from the shared library (embedded resources)
builder.AddFromAssembly(typeof(SharedLib.Marker).Assembly);
// Layer 2: base templates from this application
builder.AddFromAssembly(typeof(Program).Assembly);
// Layer 3: runtime overrides loaded from disk (e.g. customer-specific wording)
builder.AddFromFile("Localization/overrides.json", CultureInfo.InvariantCulture);
builder.AddFromFile("Localization/overrides.it-IT.json", new CultureInfo("it-IT"));
});

Because overrides.json is loaded last, any keys it defines will replace the embedded versions. Keys it does not define remain untouched.

Loading Templates

The JsonLocalizationRegistryBuilder provides several methods for loading templates. All methods return the builder instance for chaining.

AddFromAssembly

Loads embedded resources from an assembly whose names contain one of the given patterns (or the defaults localized-messages and error-messages). Culture is extracted automatically from the filename.

// Use default patterns
builder.AddFromAssembly(typeof(Program).Assembly);
// Use custom patterns
builder.AddFromAssembly(typeof(Program).Assembly,
"localized-messages", "error-messages", "notification-messages");

AddFromAssemblyTree

Scans the given assembly and all its transitive referenced assemblies. Dependencies are loaded in topological order (deepest first), so the root assembly’s templates override its dependencies’. System assemblies (System.*, Microsoft.*, netstandard) are automatically excluded.

builder.AddFromAssemblyTree(typeof(Program).Assembly);
// With custom patterns
builder.AddFromAssemblyTree(typeof(Program).Assembly, "notification-messages");

See Additive Configuration for layering semantics.

AddFromFile

Loads a single JSON file from the filesystem. You must specify the culture explicitly:

builder.AddFromFile("Resources/custom-messages.json", CultureInfo.InvariantCulture);
builder.AddFromFile("Resources/custom-messages.it-IT.json", new CultureInfo("it-IT"));

AddFromLoadedAssemblies

Scans all currently loaded assemblies whose AssemblyName starts with one of the given prefixes and loads their embedded resources. The first parameter is a string[] of prefixes; optional additional params string[] patterns override the defaults:

// Load templates from all "MyApp.*" assemblies
builder.AddFromLoadedAssemblies(["MyApp."]);
// Multiple prefixes
builder.AddFromLoadedAssemblies(["MyApp.", "MyCompany.Shared."]);
// With custom resource patterns
builder.AddFromLoadedAssemblies(["MyApp."], "notification-messages", "domain-messages");

AddFromAssemblies

Loads embedded resources from an explicit collection of assemblies, with optional custom patterns:

var assemblies = new[] { typeof(OrderError).Assembly, typeof(AuthError).Assembly };
builder.AddFromAssemblies(assemblies);
// With custom patterns
builder.AddFromAssemblies(assemblies, "localized-messages", "domain-messages");

ASP.NET Core Integration

With the Bogoware.Localization.AspNetCore package, use AddBogowareLocalization which combines registry configuration with middleware setup:

builder.Services.AddBogowareLocalization(
registry: b =>
{
b.AddFromAssembly(typeof(Program).Assembly);
b.AddFromFile("Localization/overrides.json", CultureInfo.InvariantCulture);
});

Or the shorthand overload for assembly-only setups:

builder.Services.AddBogowareLocalization(typeof(Program).Assembly);

See the ASP.NET Core Integration guide for full details on middleware configuration.

Builder Chaining

All builder methods return the builder instance, so calls can be chained fluently:

services.AddLocalization(builder => builder
.AddFromLoadedAssemblies(["MyApp."])
.AddFromAssembly(typeof(Program).Assembly, "notification-messages")
.AddFromFile("Localization/overrides.json", CultureInfo.InvariantCulture)
.AddFromFile("Localization/overrides.it-IT.json", new CultureInfo("it-IT")));

Embedding Resources

Add the JSON files as embedded resources in your .csproj:

<ItemGroup>
<EmbeddedResource Include="Resources\**\*.json" WithCulture="false" />
</ItemGroup>