TypealizR.CodeFirst.Abstractions 0.1.0-pre01

This is a prerelease version of TypealizR.CodeFirst.Abstractions.
There is a newer version of this package available.
See the version list below for details.
The owner has unlisted this package. This could mean that the package is deprecated, has security vulnerabilities or shouldn't be used anymore.
dotnet add package TypealizR.CodeFirst.Abstractions --version 0.1.0-pre01
NuGet\Install-Package TypealizR.CodeFirst.Abstractions -Version 0.1.0-pre01
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="TypealizR.CodeFirst.Abstractions" Version="0.1.0-pre01">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add TypealizR.CodeFirst.Abstractions --version 0.1.0-pre01
#r "nuget: TypealizR.CodeFirst.Abstractions, 0.1.0-pre01"
#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.
// Install TypealizR.CodeFirst.Abstractions as a Cake Addin
#addin nuget:?package=TypealizR.CodeFirst.Abstractions&version=0.1.0-pre01&prerelease

// Install TypealizR.CodeFirst.Abstractions as a Cake Tool
#tool nuget:?package=TypealizR.CodeFirst.Abstractions&version=0.1.0-pre01&prerelease

build Coverage Status CodeQL Publish

Quality Gate Status Lines of Code Duplicated Lines (%) Code Smells Maintainability Rating Security Rating Bugs Vulnerabilities Reliability Rating Technical Debt

Samples STS Samples LTS

NuGet Nuget NuGet (preview)

TypealizR

The typed internationalizeR

Statically typed i18n support for the .NET - ecosystem

usage

resource-first

✔️ DO this:


@inject IStringLocalizer<HomePage> localize;
@inject AppUser user;

<h1>@localize.Title()<h1>
<h2>@localize.Welcome_back__userName(user.GivenName)<h2>

DON´T do that:


@inject IStringLocalizer<HomePage> localize;
@inject AppUser user;

<h1>@localize["Title"]<h1>
<h2>@localize["Welcome back, {0}", user.GivenName]<h2>

code-first

✔️ DO this:

void Demo(ILocalizables i18n)
{
    Console.WriteLine(i18n.Hello("Earth")); // Hello Earth
    Console.WriteLine(i18n.Farewell("Arthur")); // So long, 'Arthur'. And thx for all the fish!
    Console.WriteLine(i18n.WhatIsTheMeaningOfLifeTheUniverseAndEverything); // 42
    Console.WriteLine(i18n.Greet(right: "Zaphod", left: "Arthur")); // Arthur greets Zaphod, and Zaphod replies: "Hi!".
}

DON´T do that:

void Demo(IStringLocalizer i18n) { Console.WriteLine(i18n["Hello", "Earth"]); // Hello Earth Console.WriteLine(i18n["Farewell", "Arthur"]); // So long, 'Arthur'. And thx for all the fish! Console.WriteLine(i18n["WhatIsTheMeaningOfLifeTheUniverseAndEverything"]; // 42 Console.WriteLine(i18n["Greet", "Arthur", "Zaphod")); // Arthur greets Zaphod, and Zaphod replies: "Hi!". }

getting started

  • install via NuGet

resource-first

  • modify target csproj (where those precious ResX-files are ;P)
<PropertyGroup>
    
    <AdditionalFileItemNames>$(AdditionalFileItemNames);EmbeddedResource</AdditionalFileItemNames>
</PropertyGroup>
  • rebuild target csproj

    NOTE: visual-studio might need a fresh restart after installing (or updating) TypealizR in order to work as expected

  • start utilizing statically typed resources demo_typealize_translation_initial

code-first

  • install via NuGet

how it works

resource-first

TypealizR parses ordinary Resx-files and generates extension-classes and -methods using source-generators on the fly.

given the following folder-structure:

root/
+---/Pages/
     +---/HomePage.razor
     +---/HomePage.resx
     +---/HomePage.en-EN.resx
     +---/HomePage.de.resx

where HomePage.resx looks like this:

key value
Title Home
Welcome back, {userName}, this is your {visitCount:i} visit Welcome back, {0}, this is your {1} visit to the app
Good bye, {userName:s} See you later, {0}

TypealizR emits the following class (comments, usings, etc. omitted):


internal static class IStringLocalizerExtensions_Root_Pages_HomePage 
{
    public static string Title(
        this IStringLocalizer<Root.Pages.HomePage> that) 
        => that["Title"];
        
    public static string Welcome_back__userName_this_is_your__visitCount__visit(
        this IStringLocalizer<Root.Pages.HomePage> that, object userName, int visitCount) 
            => that["Welcome back, {0}, this is your {1} visit to the app", userName, visitCount];
        
    public static string Good_bye__userName(
        this IStringLocalizer<Root.Pages.HomePage> that, string userName) 
            => that["See you later, {0}", userName];
}

which then can be used in favor of the lesser-typed default-syntax of IStringLocalizer<T>

type-annotations ftw

TypealizR assists in spotting translations where specified arguments may mismatch by type / order.

✔️ DO this:

Consider the following call, which might have been wrong right from the start or just became wrong over time.

When applying type-annotations to the parameters, these kind of bugs can be prevented and addressed at compile-time when combined with TypealizRs generated extension methods!

some.resx

  <data name="Hello {user:s}, it is {today:d}" xml:space="preserve">
  	<value>Hello {0}, today is {1}</value>
  </data>

somecode.cs

  var userName = "Arthur";
  var today = DateOnly.FromDateTime(DateTimeOffset.Now.UtcDateTime);

  localize.Hello__user__it_is__today(today, userName); 
 // wrong ordering, which would result in the translated string "Hello 2022-01-01, today is Arthur"

 // equivalent of localize["Hello {user:s}, it is {today:d}", today, userName]; 

With applied type-annotations, this will generate tho following compile-time errors:

  • CS1503 Argument 2: cannot convert from 'System.DateOnly' to 'string'
  • CS1503 Argument 3: cannot convert from 'string' to 'System.DateOnly'

demo_typed_parameters

DON´T do that:

There's no way the default usage of IStringLocalizer would discover such things this early in the dev-cycle!

some.resx

  <data name="Hello {user}, it is {today}" xml:space="preserve">
  	<value>Hello {0}, today is {1}</value>
  </data>

somecode.cs

  var userName = "Arthur";
  var today = DateOnly.FromDateTime(DateTimeOffset.Now.UtcDateTime);

  localize["Hello {user}, it is {today}", today, userName]; 
  // wrong parameter-ordering, which would result in the translated string "Hello 2022-01-01, today is Arthur"

Groupings

Grouping resources allows to semantically tie together resources in a meaningful way. To group resources, prepend resource-keys with [Some.Nested.Group.Name]:

demo_groups

SomeResource.resx

  <data name="[Messages.Warnings]: Attention}" xml:space="preserve">
  	<value>Attention Message</value>
  </data>
  <data name="[Messages.Warnings]: {Operation:s} failed" xml:space="preserve">
  	<value>Operation '{0}' failed</value>
  </data>
✔️ DO this:
Imperative usage

Wherever code may depend on IStringLocalizer<T>, you can do this:

    IStringLocalizer<SomeResource> localizer...; //wherever that instance might came from, most probably through dependency-injection
    var typealized = localizer.Typealize(); //call the generated extension-method, which returns a type exposing groups as properties

    //start using groups
    Console.WriteLine(typealized.Messages.Warnings.Attention); 
    // "Attention Message"

    Console.WriteLine(typealized.Messages.Warnings.Operation__failed("some operation name"); 
    // "Operation 'some operation name' failed"

The generated classes are currently duck-typing IStringLocalizer<T>. This is done to support gradually adopting the benefits of statically typed localizations, while still beeing able to use the lesser typed default way of using IStringLocalizer<T> during the course of adoption.

    IStringLocalizer<SomeResource> localizer...;
    var typealized = localizer.Typealize();

    void SomeMethod(IStringLocalizer<SomeResource> localizer) {
        //use localizer
    }
    
    SomeMethod(typealized.Localizer); //still works

Even ordinary usage is still possible:

    IStringLocalizer<SomeResource> localizer...;
    var typealized = localizer.Typealize();
    
    localizer["[Messages.Warnings]: {Operation:s} failed", "some operation"];
    typealized["[Messages.Warnings]: {Operation:s} failed", "some operation"]; //still works
Microsoft.Extensions.DependencyInjection
manual setup
//normal setup
var services = new ServiceCollection();
services.AddLogging();
services.AddLocalization();

//register typealized resource
services.AddScoped(x => x.GetRequiredService<IStringLocalizer<Resources>>().Typealize());

var provider = services.BuildServiceProvider();
using var scope = provider.CreateScope();

//service-located typealized instance (or better just inject it somewhere)
var typealized = scope.ServiceProvider.GetRequiredService<TypealizedResources>();

The generated types are placed in a seperated namespace to prevent collisions with other types. Given a *.resx-file with the following FullName: Some\Folder\Path\Resources.resx

The generated type will be Some.Folder.Path.TypealizR.TypealizedResources

automatic setup

tbd. There might be a built-in solution for utilizing IServiceCollection to register typealized instances, once #63 is done.

DON'T DO this:

All groups are still available as extension-methods for IStringLocalizer<T> as a list of flat members.


IStringLocalizer<SomeResource> localize...;

Console.WriteLine(localize.MessagesWarnings_Attention()); 
// "Attention Message"


Console.WriteLine(localize.MessagesWarnings_Operation__failed("some operation name"); 
// "Operation 'some operation name' failed"

Custom Tool Namespaces

If the consuming project of TypealizR utilizes resx-files which specify a CustomToolNameSpace demo_CustomToolNamespace

, the source-generator may produce invalid source-code, unless the following modifications where made within the consuming csproj-file:

<ItemGroup>
	<CompilerVisibleItemMetadata Include="EmbeddedResource" MetadataName="CustomToolNamespace" />
</ItemGroup>

This is due to the fact that source-generators may not see msbuild-properties unless developers explicitly opt-in to let source-generators see those custom properties on a per-file-basis. The above options makes the CustomToolNamespace-property of any EmbeddedResource visible to the source-generator, so that the generated types may be placed there. See Consume MSBuild properties and metadata for further details.

code-first

  • Author an ordinary interface, marked with CodeFirstTypealizedAttribute somewhere within your project.
  • Use properties for plain translatables.

    return-type needs to be LocalizedString

  • Use methods for type-safe translation of formatted translatables.

    return-type needs to be LocalizedString

  • Utilize structured xml comments to provide custom default-values.
[CodeFirstTypealized]
public interface ILocalizables
{
    LocalizedString Hello(string world);

    /// <summary>
    /// 42
    /// </summary>
    LocalizedString WhatIsTheMeaningOfLifeTheUniverseAndEverything { get; }

    /// <summary>
    /// So long, '<paramref name="user"/>'. And thx for all the fish!
    /// </summary>
    /// <param name="user"></param>
    /// <returns></returns>
    LocalizedString Farewell(string user);

    /// <summary>
    /// <paramref name="left"/> greets <paramref name="right"/>, and <paramref name="right"/> replies: "Hi!".
    /// </summary>
    /// <param name="left"></param>
    /// <param name="right"></param>
    /// <returns></returns>
    LocalizedString Greet(string left, string right);
}

Based on such an interface, TypealizR will generate a default implementation, which easily can be dependency injected:

void Demo(ILocalizables i18n)
{
    Console.WriteLine(i18n.Hello("Earth")); // Hello Earth
    Console.WriteLine(i18n.Farewell("Arthur")); // So long, 'Arthur'. And thx for all the fish!
    Console.WriteLine(i18n.WhatIsTheMeaningOfLifeTheUniverseAndEverything); // 42
    Console.WriteLine(i18n.Greet(right: "Zaphod", left: "Arthur")); // Arthur greets Zaphod, and Zaphod replies: "Hi!".
}

synchronize resources

not supported, yet. But will be awesome 😉

extensibilty

customize string formatting

Starting with v0.6, TypealizR supports customizing the internal usage of string.Format(), which should enable developers to implement #16 with the technology / library / approach of their choice - Leaving TypelaziR un-opinionated about the actual approach to achieve this.

To customize the formatting, just drop a custom implementation of TypealizR_StringFormatter anywhere in the project. The types namespace MUST match the project´s root-namespace.

example

Given the root-namespace TypealizR.Ockz for the project consuming TypealizR, this partial-class declaration should be enough:

namespace TypealizR.Ockz;
internal static partial class TypealizR_StringFormatter
{
    internal static partial string Format(string s, object[] args) => 
        new(string.Format(s, args).Reverse().ToArray());
}

With this implementation, every localized string would be reversed. (Even if that doesn´t make any sense ;P)

code-first

tbd

configuration

parameter names in method names

Per default, TypealizR uses any given parameter name within the name of generated methods.

f.e. the resource-key Hello {world:s} will be populated as Hello__world(string world).

For some naming-strategies, this could be a bit too verbose, so you can opt-out of this behavior either globally or on a per-file basis. f.e. the resource-key Hello {world:s} will then be populated as Hello(string world).

Both approaches require modifications within the consuming *.csproj-files:

global

<ItemGroup>
	<CompilerVisibleProperty Include="TypealizR_UseParamNamesInMethodNames" />
</ItemGroup>

<PropertyGroup>
	<TypealizR_UseParamNamesInMethodNames>false</TypealizR_UseParamNamesInMethodNames>
</PropertyGroup>

per file

<ItemGroup>
	<CompilerVisibleItemMetadata Include="EmbeddedResource" MetadataName="TypealizR_UseParamNamesInMethodNames" />
</ItemGroup>

<EmbeddedResource Update="Some.resx">
	<TypealizR_UseParamNamesInMethodNames>false</TypealizR_UseParamNamesInMethodNames>
</EmbeddedResource>

the per-file setting takes precedence over the global setting. So you can choose to just opt-out on a per-file basis, or opt-out globally and optin-in on a per-file basis, if needed

customize warnings

During code-generation, the code-generator might emit one of these diagnostics.

To modify the severity of each reported diagnostics, provide a .globalconfig-file in the root directory of the project which consumes TypealizR.

samples

To ignore all diagnostics emitted by TypealizR, provide the following content to .globalconfig:

is_global = true
dotnet_diagnostic_TR0002_severity = hidden
dotnet_diagnostic_TR0003_severity = hidden
dotnet_diagnostic_TR0004_severity = hidden

To treat all diagnostics emitted by TypealizR as a compiler-error, supply the following contents:

is_global = true
dotnet_diagnostic_TR0002_severity = error
dotnet_diagnostic_TR0003_severity = error
dotnet_diagnostic_TR0004_severity = error

See

  • global-analyzerconfig for further details about analyzer-configs.
  • #12 for details about design-decisssions
  • #35 for implementation-details
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. 
.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.

Version Downloads Last updated
0.9.5 166 11/6/2023
0.9.4 111 9/19/2023
0.9.3 122 5/13/2023
0.9.2 220 2/5/2023
0.9.2-pre0008 661 2/5/2023