Skip to content

egorozh/Slang.NET

Repository files navigation

Slang.NET

Nuget License: MIT

Type-safe i18n for .NET

About this library

Slang.NET is a .NET port of the slang from the Dart/Flutter community with new features (like string format).

You can view how the generated files general, en, and de look

Getting Started:

Install the library as a NuGet package:

Install-Package dotnet add package Slang.Net

Add JSON files:

Important file must end with ".i18n.json". This is necessary so that the SourceGenerator does not track changes to other AdditionalFiles.

i18n/strings_en.i18n.json or i18n/strings_en-US.i18n.json or i18n/strings.i18n.json (for base culture)

{
  "screen": {
    "locale1": "Locale 1"
  }
}

i18n/strings_ru.i18n.json or i18n/strings_ru-RU.i18n.json

{
  "screen": {
    "locale1": "Локаль 1"
  }
}

slang.json

{
  "base_culture": "en" // or "en-EN"
}

Recommendation It is recommended to specify the country code, such as "en-US," for proper functionality when formatting strings, especially if you will be retrieving the list of cultures from SupportedCultures.

Include JSON files as AdditionalFiles:

  <ItemGroup>
    <AdditionalFiles Include="i18n\*.i18n.json" />
    <AdditionalFiles Include="slang.json" />
  </ItemGroup>

Add a partial class:

[Translations(InputFileName = "strings")]
public partial class Strings;

Done!

Strings.SetCulture(new CultureInfo("ru-RU")); 

Console.WriteLine(Strings.Instance.Root.Screen.Locale1); // Локаль 1

Strings.SetCulture(new CultureInfo("en-US"));

Console.WriteLine(Strings.Instance.Root.Screen.Locale1); // Locale 1

or

  <MenuItem  Header="{Binding Root.Screen.Locale1, Source={x:Static localization:Strings.Instance}}" />

Features

String Interpolation

You can specify parameters passed at runtime..

"Hello": "Hello {name}"

The generated code will look like this:

/// In en, this message translates to:
/// **"Hello {name}"**
public virtual string Hello(object name) => $"Hello {name}";

Typed Parameters

Parameters are typed as object by default. This is convenient because it offers maximum flexibility.

You can specify the type using two syntax options: 1 - Simple:

{
  "greet": "Hello {name: string}, you are {age: int} years old"
}

2 - Using a placeholders, which allows you to specify a type or format string (see String Format).

{
  "greet2": "Hello {name}, you are {age} years old",
  "@greet2": {
    "placeholders": {
      "name": {
        "type": "string"
      },
      "age": {
        "type": "int"
      }
    }
  },
}

The generated code will look like this:

/// In ru, this message translates to:
/// **"Hello {name}, you are {age} years old"**
public virtual string Greet(string name, int age) => $"Hello {name}, you are {age} years old";

Comments

You can add comments to your translation files.

{
  "@@locale": "en", // fully ignored
  "mainScreen": {
    "button": "Submit",

    // ignored as translation but rendered as a comment
    "@button": "The submit button shown at the bottom",

    // or use 
    "button2": "Submit",
    "@button2": {
      "description": "The submit button shown at the bottom"
    }
  }
}

The generated code will look like this:

/// The submit button shown at the bottom
///
/// In ru, this message translates to:
/// **"Submit"**
public virtual string Button => "Submit";

String Format

This library supports embedding format via ToString(format) for the following types: int, long, double, decimal, float, DateTime, DateOnly, TimeOnly, TimeSpan. For other types, the format string is passed through string.Format(format, locale).

{
 "dateExample": "Date {date}",
    "@dateExample": {
      "placeholders": {
        "date": {
          "type": "DateTime",
          "format": "dd MMMM HH:mm"
        }
      }
    },
}
String s = Strings.Instance.Root.DateExample(DateTime.Now); // Date 17 October 22:25

The generated code will look like this:

/// In ru, this message translates to:
/// **"Date {date}"**
public virtual string DateExample(DateTime date)
{
	string dateString = date.ToString("dd MMMM HH:mm");
	return $"Date {dateString}";
}

Pluralization

This library uses the concept defined here.

Some languages have support out of the box. See here.

Plurals are detected by the following keywords: zero, one, two, few, many, other.

{
  "someKey": {
    "apple": {
      "one": "I have {n} apple.",
      "other": "I have {n} apples."
    }
  }
}
String a = Strings.Instance.Root.SomeKey.Apple(n: 1); // I have 1 apple.
String b = Strings.Instance.Root.SomeKey.Apple(n: 2); // I have 2 apples.    

The generated code will look like this:

public virtual string Apple(int n) => PluralResolvers.Cardinal("en")(n,
					one: $"I have {n} apple.",
					other: $"I have {n} apples.");

The detected plurals are cardinals by default.

To specify ordinals, you need to add the (ordinal) modifier.

{
  "someKey": {
    "apple(cardinal)": {
      "one": "I have {n} apple.",
      "other": "I have {n} apples."
    },
    "place(ordinal)": {
      "one": "{n}st place.",
      "two": "{n}nd place.",
      "few": "{n}rd place.",
      "other": "{n}th place."
    }
  }
}

By default, the parameter name is n. You can change that by adding a modifier.

{
  "someKey": {
    "apple(param=appleCount)": {
      "one": "I have one apple.",
      "other": "I have multiple apples."
    }
  }
}
String a = Strings.Instance.Root.SomeKey.Apple(appleCount: 1); // notice 'appleCount' instead of 'n'

You can set the default parameter globally using PluralParameter.

[Translations(
    InputFileName = "strings",
    PluralParameter = "count")]
internal partial class Strings;

Linked Translations

You can link one translation to another. Add the prefix @: followed by the absolute path to the desired translation.

{
  "fields": {
    "name": "my name is {firstName}",
    "age": "I am {age} years old"
  },
  "introduce": "Hello, @:fields.name and @:fields.age"
}
String s = Strings.Instance.Root.Introduce(firstName: "Tom", age: 27); // Hello, my name is Tom and I am 27 years old.

The generated code will look like this:

/// In ru, this message translates to:
/// **"Hello, {_root.Fields.Name(firstName: firstName)} and {_root.Fields.Age(age: age)}"**
public virtual string Introduce(object firstName, object age) => $"Hello, {_root.Fields.Name(firstName: firstName)} and {_root.Fields.Age(age: age)}";

Optionally, you can escape linked translations by surrounding the path with {}:

{
  "fields": {
    "name": "my name is {firstName}"
  },
  "introduce": "Hello, @:{fields.name}inator"
}

Lists

You can also place lists inside lists!

{
  "niceList": [
    "hello",
    "nice",
    [
      "first item in nested list",
      "second item in nested list"
    ],
    {
      "wow": "WOW!",
      "ok": "OK!"
    },
    {
      "aMapEntry": "access via key",
      "anotherEntry": "access via second key"
    }
  ]
}
String a = Strings.Instance.Root.NiceList[1]; // "nice"
String b = Strings.Instance.Root.NiceList[2][0]; // "first item in nested list"
String c = Strings.Instance.Root.NiceList[3].Ok; // "OK!"
String d = Strings.Instance.Root.NiceList[4].AMapEntry; // "access via key"

The generated code will look like this:

public virtual List<dynamic> NiceList => [
				"hello",
				"nice",
				new[]{
					"first item in nested list",
					"second item in nested list",
		    },
				new Feature1NiceList0i3Ru(_root),
				new Feature1NiceList0i4Ru(_root),
	];

Maps

You can access each translation using string keys.

Add the (map) modifier.

{
  "a(map)": {
    "helloWorld": "hello"
  },
  "b": {
    "b0": "hey",
    "b1(map)": {
      "hiThere": "hi"
    }
  }
}

Now you can access translations using keys:

String a = Strings.Instance.Root.A["helloWorld"]; // "hello"
String b = Strings.Instance.Root.B.B0; // "hey"
String c = Strings.Instance.Root.B.B1["hiThere"]; // "hi"

The generated code will look like this:

/// In ru, this message translates to:
/// **"hey"**
public virtual string B0 => "hey";
public virtual IReadOnlyDictionary<string, string> B1 => new Dictionary<string, string> {
					{"hiThere", "hi"},
};

Tools

Translate with GPT

Take advantage of GPT to internationalize your app with context-aware translations.

Download slang-gpt.

Then add the following configuration in your slang.json:

{
  "base_culture": "ru",
  "gpt": {
    "model": "gpt-4o-mini",
    "description": "Showcase for Slang.Net.Gpt"
  }
}

Then use slang-gpt:

<cli-directory>/slang-gpt <csproj-path> --target=ru --api-key=<api-key>

See more: Documentation