A Roslyn incremental source generator that reads JSON translation files (additional files) and generates strongly-typed C# classes for internationalization (i18n).
You must build this project to see the result (generated code) in the IDE.
Add your translation files as AdditionalFiles in your .csproj. The generator uses no.json as the base/default language (English fallback) and any other .json files (e.g. en.json) as additional locales.
The JSON files can contain nested objects (mapped to nested static classes) and string values (mapped to properties or constants). Strings support:
- Interpolation:
{paramName}placeholders become method parameters (e.g."Hello {name}"→Language.hello(object name)) - Pluralization: pipe-separated variants
"no items | one item | {count} items"— the first parameter becomesint count, and the correct variant is selected at runtime
The generator produces three generated classes from the same JSON translation pipeline:
Generated file: Language.g.cs
A strongly-typed static class that mirrors the structure of your JSON files. Each key becomes a static string property or method that returns the correct translation for CultureInfo.CurrentUICulture at runtime, falling back to English (en) if the current culture is not available.
Example — given en.json:
{
"message": { "hello": "hello world" },
"plural": { "car": "car | cars" }
}Usage:
string greeting = Language.message.hello; // plain string
string cars = Language.plural.car(3); // pluralized: "cars"Generated file: LanguageKeys.g.cs
A static class of const string keys that mirror the JSON structure. Useful for returning keys to the frontend in an API response.
Example:
string key = LanguageKeys.message.hello; // == "message.hello"Generated file: LanguageStrings.g.cs
A static class that holds all translations in a Dictionary<string, Dictionary<string, string>> (keyed by two-letter ISO language code and then by dot-separated key path). Exposes two lookup methods:
string value = LanguageStrings.GetString("message.hello"); // returns key if not found
string? value = LanguageStrings.GetStringOrNull("message.hello"); // returns null if not foundIn your .csproj, include JSON files as AdditionalFiles:
<ItemGroup>
<AdditionalFiles Include="no.json" />
<AdditionalFiles Include="en.json" />
</ItemGroup>By default the fallback language is English (en). You can change this by setting FallbackLanguage in your .csproj:
<PropertyGroup>
<FallbackLanguage>no</FallbackLanguage>
</PropertyGroup>The value must match the filename of one of your translation JSON files (without the .json extension). All three generated classes — Language, LanguageKeys, and LanguageStrings — will use this language as the default when no matching translation is found for CultureInfo.CurrentUICulture.
The generated classes default to the consuming project's RootNamespace. You can override that with GeneratedNamespace:
<PropertyGroup>
<GeneratedNamespace>MyApp.Localization</GeneratedNamespace>
</PropertyGroup>With that configuration, the generator emits MyApp.Localization.Language, MyApp.Localization.Language, and MyApp.Localization.LanguageStrings instead of generating them in the global namespace.
Note for project references: When referencing the generator as a
ProjectReference(rather than via NuGet), import the props file manually soFallbackLanguageandGeneratedNamespaceare visible to the analyzer:<Import Project="..\BccCode.I18N.SourceGen\build\BccCode.I18N.SourceGen.props" />When consumed via NuGet, this import happens automatically.
The NuGet package is built as an analyzer package:
BccCode.I18N.SourceGen.dllis packed underanalyzers/dotnet/cs- normal
lib/output is disabled so consumers do not reference the generator assembly directly build/BccCode.I18N.SourceGen.propsis included soFallbackLanguageremains compiler-visible for package consumers
The repository contains two integration test projects under BccCode.I18N.SourceGen/BccCode.I18N.SourceGen:
BccCode.I18N.SourceGen.IntegrationTeststests consumption through an analyzer-styleProjectReferenceBccCode.I18N.SourceGen.NuGetIntegrationTeststests the packed.nupkgthroughPackageReference
Use the following commands from the nested repository root BccCode.I18N.SourceGen/BccCode.I18N.SourceGen:
dotnet test .\BccCode.I18N.SourceGen.IntegrationTests\BccCode.I18N.SourceGen.IntegrationTests.csproj
dotnet pack .\BccCode.I18N.SourceGen\BccCode.I18N.SourceGen.csproj -c Release -o .\artifacts
Remove-Item .\packages -Recurse -Force -ErrorAction SilentlyContinue
dotnet restore .\BccCode.I18N.SourceGen.NuGetIntegrationTests\BccCode.I18N.SourceGen.NuGetIntegrationTests.csproj --packages .\packages --configfile .\nuget.integration-tests.config --force
dotnet build .\BccCode.I18N.SourceGen.NuGetIntegrationTests\BccCode.I18N.SourceGen.NuGetIntegrationTests.csproj -c Release --packages .\packages --no-restore
dotnet test .\BccCode.I18N.SourceGen.NuGetIntegrationTests\BccCode.I18N.SourceGen.NuGetIntegrationTests.csproj -c Release --no-build --no-restoreThis mirrors the source-generator publishing flow described by Andrew Lock: first verify compiler integration through an analyzer-style project reference, then verify the actual packed analyzer from a local NuGet source without polluting the machine-wide NuGet cache.
Clearing the local packages folder before restore is important when reusing the same package version locally, otherwise NuGet can keep an older packed analyzer and stale build props.