Senlinz.Localization 3.5.0

dotnet add package Senlinz.Localization --version 3.5.0
                    
NuGet\Install-Package Senlinz.Localization -Version 3.5.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Senlinz.Localization" Version="3.5.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Senlinz.Localization" Version="3.5.0" />
                    
Directory.Packages.props
<PackageReference Include="Senlinz.Localization" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Senlinz.Localization --version 3.5.0
                    
#r "nuget: Senlinz.Localization, 3.5.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Senlinz.Localization@3.5.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Senlinz.Localization&version=3.5.0
                    
Install as a Cake Addin
#tool nuget:?package=Senlinz.Localization&version=3.5.0
                    
Install as a Cake Tool

Senlinz.Localization

English | Chinese

A JSON-driven localization source generator for .NET that generates strongly typed localization accessors, resource base classes, and enum-to-localization helpers.

Build-time requirement: .NET 8 SDK or newer (the source generator depends on Roslyn 4.8).

Runtime compatibility: generated runtime support targets netstandard2.0, so it can run on .NET Framework 4.6.1+, .NET Core 2.0+, and newer runtimes.

Note: using more traditional C# syntax mainly reduces compiler and tooling requirements; actual runtime compatibility still comes from the netstandard2.0 target.

Quick navigation

Features

  • Generate L accessors from a primary culture JSON file.
  • Generate LResource plus one concrete resource class for every discovered culture JSON file.
  • Resolve localized text through LString, LStringResolver, and generated LResource types.
  • Convert enum values to localization keys with [LString] and [LStringKey].
  • Publish Senlinz.Localization and Senlinz.Localization.Abstractions as separate NuGet packages.

Package selection

Senlinz.Localization

Use this package in consumer projects that need source generation from JSON.

dotnet add package Senlinz.Localization

Projects that need their own generated L.g.cs should always reference Senlinz.Localization directly. Since 3.4.0, transitive references no longer import the localization build props, so direct references remain the intended setup.

Senlinz.Localization.Abstractions

Use this package only when you need the shared localization contracts without the source generator.

dotnet add package Senlinz.Localization.Abstractions

Quick start

1. Create the localization files

Place localization JSON files under the L/ folder by default. en.json is the default primary file unless you override it with SenlinzLocalizationFile.

Example layout:

MyProject/
├── L/
│   ├── en.json
│   └── zh.json
└── MyProject.csproj
// en.json
{
  "hello": "Hello",
  "sayHelloTo": "Hello {name}!",
  "statusReady": "Ready",
  "userType": {
    "teacher": "Teacher",
    "student": "Student"
  }
}
// zh.json
{
  "hello": "你好",
  "sayHelloTo": "你好,{name}!",
  "statusReady": "就绪",
  "userType": {
    "teacher": "老师",
    "student": "学生"
  }
}

2. Add the files to the localization folder

<ItemGroup>
  <AdditionalFiles Include="L/**/*.json" />
</ItemGroup>
  • For direct package references, the package automatically adds $(SenlinzLocalizationFolder)/**/*.json to AdditionalFiles, so files placed under L/ are picked up without extra project configuration.
  • The explicit AdditionalFiles entry above is only needed when you want to override or extend the default item inclusion behavior.
  • The generator only reads JSON files under the configured localization folder, including subfolders.

3. Use generated members

After build, the generator creates strongly typed members from each JSON key.

Console.WriteLine(L.Hello);
Console.WriteLine(L.SayHelloTo("World"));
  • hello becomes L.Hello.
  • sayHelloTo becomes L.SayHelloTo(string name).

4. Use generated culture resources and resolve text

using Senlinz.Localization;

var currentCulture = "zh";
var resolver = new LStringResolver(() => currentCulture);

Console.WriteLine(resolver[L.Hello]);
Console.WriteLine(resolver[L.SayHelloTo("世界")]);
  • new LStringResolver(() => currentCulture) uses the generated resolver and automatically includes all discovered resources.
  • If you need runtime overrides, pass your own LResource instances to the overload that accepts resources explicitly.

Localization file rules

Key format

  • JSON keys are converted into generated C# member names.
  • Keep keys stable because generated API names depend on them.
  • Generated member names follow the JSON shape directly and only capitalize the leading letter to fit Pascal-style naming, so user_status becomes L.User_status.
  • Nested JSON objects generate nested accessors, so exception -> user -> notFound becomes L.Exception.User.NotFound(...).
  • Nested JSON paths use dotted keys internally, so the example above resolves as exception.user.notFound.
  • Enum keys use a nested path based on the enum name and member name, so UserType.Teacher resolves to userType.teacher and L.UserType.Teacher.

Placeholder parameters

Placeholders inside values become method parameters.

{
  "welcomeUser": "Welcome {userName}",
  "orderSummary": "Order {orderId} for {customerName}"
}

Generated usage:

var message1 = L.WelcomeUser("Alice");
var message2 = L.OrderSummary("SO-001", "Alice");

Escaping placeholders

  • If you want to keep braces as literal text instead of generating a parameter, prefix the placeholder name with $.
{
  "templateTip": "Use {$name} as a placeholder in your template."
}
  • The generated default text becomes Use {name} as a placeholder in your template.

Primary localization file

SenlinzLocalizationFile selects which JSON file generates L and which generated resource acts as the default resource. The default is en.json.

<PropertyGroup>
  <SenlinzLocalizationFolder>L</SenlinzLocalizationFolder>
  <SenlinzLocalizationFile>zh.json</SenlinzLocalizationFile>
</PropertyGroup>

<ItemGroup>
  <AdditionalFiles Include="L/**/*.json" />
</ItemGroup>

Missing the configured primary file does not report SL004 by default. If you want that warning, opt in explicitly:

<PropertyGroup>
  <SenlinzLocalizationWarnMissingPrimaryFile>true</SenlinzLocalizationWarnMissingPrimaryFile>
</PropertyGroup>

Localization folder

SenlinzLocalizationFolder selects which folder the generator scans for localization JSON files. The default is L, and all nested subfolders are included.

<PropertyGroup>
  <SenlinzLocalizationFolder>Localization</SenlinzLocalizationFolder>
  <SenlinzLocalizationFile>en.json</SenlinzLocalizationFile>
</PropertyGroup>

<ItemGroup>
  <AdditionalFiles Include="**/*.json" />
</ItemGroup>
  • Only JSON files under the configured folder are treated as localization inputs.
  • This lets you keep other JSON files in AdditionalFiles without the generator trying to use them as localization resources.

Generated types

L

  • L contains strongly typed accessors for every key in the localization JSON.
  • Plain values generate properties, and values with placeholders generate methods.

LResource

  • LResource is a generated abstract base class whose GetResource() method returns the primary localization dictionary.
  • The generator also emits one concrete internal *Resource class per discovered culture JSON file, such as EnResource and ZhResource.
  • new LStringResolver(() => currentCulture) uses the generated resolver and wires in every discovered resource automatically.
  • You can still derive your own custom resources from LResource and override GetResource() when you need runtime overrides.

LString

  • LString carries the localization key, fallback text, and runtime arguments.
  • You normally get LString values from generated L members or from enum extensions.

Resolve localized values

Use LStringResolver to resolve text for the current culture. For most applications, new LStringResolver(...) is the simplest setup.

using Senlinz.Localization;

var currentCulture = "zh";
var resolver = new LStringResolver(() => currentCulture);

Console.WriteLine(resolver[L.Hello]);
Console.WriteLine(resolver[L.SayHelloTo("世界")]);

If you need runtime overrides, derive from LResource, override GetResource(), and pass your own resources explicitly.

You can also call the instance method:

var text = resolver.Resolve(L.Hello);

Fallback behavior

  • If no resource exists for the current culture, the default text from the primary JSON file is used.
  • If a resource exists but does not contain a key, the default text is also used.
  • Resource dictionaries are cached per resolver instance and culture.

Enum localization

[LString]

Apply [LString] to an enum to generate a ToLString() extension method.

[LString]
public enum UserType
{
    Teacher,
    Student
}
  • This generates UserTypeExtensions.ToLString(this UserType value).
  • Enum values always use the nested key pattern <enumNameCamelCase>.<memberNameCamelCase>.
  • For UserType.Teacher, the generated key is userType.teacher, so the accessor is L.UserType.Teacher.

For the enum above, the expected localization keys are typically:

{
  "userType": {
    "teacher": "Teacher",
    "student": "Student"
  }
}

[LStringKey]

Use [LStringKey] on enum members when you want to override only the enum member segment of the key.

[LString]
public enum UserType
{
    [LStringKey("teacher")]
    Teacher,

    [LStringKey("student")]
    Student
}

[LStringKey] only replaces the final enum member segment. The enum prefix segment stays derived from the enum name.

Matching JSON:

{
  "userType": {
    "teacher": "Teacher",
    "student": "Student"
  }
}

Passing a dotted or legacy full key still only changes the final member segment:

[LString]
public enum UserType
{
    [LStringKey("userType.teacher")]
    Teacher,

    [LStringKey("legacy.student")]
    Student
}

Usage:

var text = UserType.Student.ToLString();
Console.WriteLine(resolver[text]);

End-to-end example

en.json

{
  "hello": "Hello",
  "sayHelloTo": "Hello {name}!",
  "userType": {
    "teacher": "Teacher",
    "student": "Student"
  }
}

zh.json

{
  "hello": "你好",
  "sayHelloTo": "你好,{name}!",
  "userType": {
    "teacher": "老师",
    "student": "学生"
  }
}

Enum

[LString]
public enum UserType
{
    Teacher,
    Student
}

Resolver

using Senlinz.Localization;

var currentCulture = "zh";
var resolver = new LStringResolver(() => currentCulture);

Console.WriteLine(resolver[L.Hello]);
Console.WriteLine(resolver[L.SayHelloTo("世界")]);
Console.WriteLine(resolver[UserType.Student.ToLString()]);

Expected output:

你好
你好,世界!
学生

Release and documentation publishing

  • The latest published NuGet release is 3.5.0.
  • Keep README.md, README.zh-CN.md, docs/README.md, and docs/zh-CN/README.md aligned so the repository and Docsify site show the same release status.
  • Record package-facing changes in CHANGELOG.md and RELEASE_NOTES.md before creating the next release tag.
  • Publishing v* or V* tags triggers the NuGet publish workflow, and updates to the docs/ content are deployed through the documentation workflow.
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Makes the SL004 missing-primary-file diagnostic explicit opt-in while keeping automatic JSON discovery for direct package references.