Translation files use the .tkeys extension and follow an INI-inspired format: one entry per line, with optional sections and typed placeholders.
Every .tkeys file must start with an @class directive that specifies the generated class name:
@class AppThis generates public static partial class App. The directive is required — the file will not compile without it.
A translation file is a plain text file. Each line is one of:
| Line type | Example | What it does |
|---|---|---|
| Class directive | @class App |
Sets the generated class name (required, once per file) |
| Comment | # This is a comment |
Ignored entirely |
| Section | [Navigation] |
Groups the keys below it into a nested class |
| Entry | Home : Home |
Defines a translation key with its fallback text |
| Blank line | (empty) | Ignored (use freely for readability) |
@class App
# This file defines all user-facing strings for the app
[Navigation]
Home : Home
Settings : Settings
[Errors]
NotFound : Page not foundThe file name is not used for class naming — the @class directive controls the generated class name. You can name files however you like:
| File | @class directive |
Generated class |
|---|---|---|
app-strings.tkeys |
@class App |
public static partial class App |
errors.tkeys |
@class Errors |
public static partial class Errors |
admin.tkeys |
@class AdminPanel |
public static partial class AdminPanel |
You can have multiple .tkeys files in one project. Each generates its own class.
Sections are optional. They group keys into nested static classes:
[Buttons]
Save : Save
Cancel : CancelGenerates:
public static partial class App
{
public static partial class Buttons
{
public static TranslationString Save { get; } = ...;
public static TranslationString Cancel { get; } = ...;
}
}
// Usage:
string label = App.Buttons.Save;Keys defined before any section go into the root class:
AppName : My App
[Buttons]
Save : SaveApp.AppName // "My App" (root level)
App.Buttons.Save // "Save" (inside section)Section names must be valid C# identifiers (letters, digits, underscores; no spaces).
Placeholders let you inject values into translation strings. They use curly braces with a name and an optional type (separated by |):
{parameterName}
{parameterName|Type}
When the type is omitted, the parameter defaults to object.
Example:
Greeting : Hello, {userName|string}!
Message : The value is {data}This generates methods (not properties):
// Generated:
public static TranslationString Greeting(string userName) => ...;
public static TranslationString Message(object data) => ...;
// Usage:
string msg = App.Greeting("Marco");
// "Hello, Marco!"An entry can have multiple placeholders:
Summary : {userName|string} has {count|int} items worth {total:C2|decimal}string msg = App.Summary("Marco", 3, 29.99m);
// "Marco has 3 items worth $29.99"Parameters appear in the generated method in the same order as in the text.
You can add a format specifier between the name and the type, using : for the format and | for the type:
{parameterName:formatString|Type}
The : separates the name from the format (just like standard .NET formatting), and | separates the format from the type. This means format strings containing colons (like HH:mm:ss) work naturally.
Examples:
# DateTime formatted as hours:minutes
TimeNow : It's {time:HH:mm|DateTime}
# Decimal formatted as currency with 2 decimal places
Price : Total: {price:C2|decimal}
# Integer formatted with thousands separator
BigNumber : Population: {count:N0|int}The format specifier works like .NET's standard format strings. It is used as the default when formatting the fallback text. Translators can override it in their translation files (see Runtime Configuration).
These type names can be used in placeholders:
| Type alias | .NET type | Supports format specifiers |
|---|---|---|
| (omitted) | System.Object |
At runtime, if value is IFormattable |
object |
System.Object |
At runtime, if value is IFormattable |
string |
System.String |
No |
int |
System.Int32 |
Yes |
long |
System.Int64 |
Yes |
float |
System.Single |
Yes |
double |
System.Double |
Yes |
decimal |
System.Decimal |
Yes |
bool |
System.Boolean |
No |
DateTime |
System.DateTime |
Yes |
DateTimeOffset |
System.DateTimeOffset |
Yes |
DateOnly |
System.DateOnly |
Yes |
TimeOnly |
System.TimeOnly |
Yes |
TimeSpan |
System.TimeSpan |
Yes |
Guid |
System.Guid |
Yes |
You can also use fully-qualified type names for types not in this list.
If you need a literal { or } in your text, double them:
CodeExample : Use {{braces}} to define placeholdersThis produces the text: Use {braces} to define placeholders
This works the same way as C# string interpolation escaping.
Here's a complete example showing input and output:
Input (App.tkeys):
@class App
# Simple key (no placeholders)
AppName : My Application
[Greetings]
# Simple key inside a section
Hello : Hello!
# One string parameter
Welcome : Welcome, {name|string}!
# Multiple parameters with format specifiers
DetailedGreeting : Good {timeOfDay|string}, {name|string}! It's {time:HH:mm|DateTime}.
# Typeless parameter (defaults to object)
Generic : The value is {data}Generated API:
// Simple keys become properties
TranslationString name = App.AppName; // "My Application"
string nameStr = App.AppName; // implicit conversion
// Section keys are nested
TranslationString hello = App.Greetings.Hello; // "Hello!"
// Parameterized keys become methods
TranslationString ts = App.Greetings.Welcome("Marco"); // "Welcome, Marco!"
// Multiple params
string msg = App.Greetings.DetailedGreeting(
"morning",
"Marco",
new DateTime(2026, 2, 7, 9, 30, 0));
// "Good morning, Marco! It's 09:30."
// Typeless params accept any object
string generic = App.Greetings.Generic(42); // "The value is 42"- One entry per line — use
\at the end of a line for continuation (see below) - Keys must be unique within a file (across all sections)
- Key names should be valid C# identifiers (PascalCase recommended)
- Section names should be valid C# identifiers
- Parameter names should be valid C# identifiers (camelCase recommended)
- The
:separator is the first:on the line — values can contain: - Leading/trailing whitespace around keys and values is trimmed
For long translation text, use \ (backslash) at the end of a line to continue on the next line. Leading whitespace on continuation lines is trimmed:
[Messages]
LongDescription : This is a very long description \
that spans multiple lines \
for readability
ShortOne : This fits on one lineThe result is a single value: This is a very long description that spans multiple lines for readability.
Notes:
- The
\must be the last character on the line - Leading whitespace on continuation lines is trimmed — add a space before
\if you want a space between parts - Backslashes that are not the last character are treated as literal characters (e.g.,
C:\Usersworks as expected) - Continuation works with placeholders:
{name|string}can span across lines
To embed an actual newline character in the translated text, use \n:
[Messages]
Terms : By continuing you agree to:\nRule 1: No spamming\nRule 2: Be respectfulResult (3 lines):
By continuing you agree to:
Rule 1: No spamming
Rule 2: Be respectful
This composes with \ continuation for readability:
Terms : By continuing you agree to:\n\
Rule 1: No spamming\n\
Rule 2: Be respectfulEscape sequences:
| Sequence | Result |
|---|---|
\n |
Newline character |
\r |
Carriage return character |
\\ |
Literal backslash |
\ (end of line) |
Line continuation (join) |
\X (other) |
Kept as-is (e.g., C:\Users is safe) |
Both \n and \r\n are normalized to Environment.NewLine at resolve time, so the output matches the current platform regardless of how the newline was written.
To produce a literal \n in the output (not a newline), write \\n.
The source generator reports validation issues directly in the IDE (squiggly underlines in VS/Rider) and in CI build output. No extra tooling required.
| ID | Severity | Condition | Example message |
|---|---|---|---|
| TKEYS001 | Error | Missing @class directive |
"Translation file 'app.tkeys' is missing a required '@class ClassName' directive" |
| TKEYS002 | Error | Duplicate key in same file | "Key 'Welcome' is defined more than once" |
| TKEYS003 | Error | Unknown placeholder type | "Unknown type 'strng' in placeholder {name|strng}" |
| TKEYS004 | Warning | Empty section with no entries | "Section [Empty] contains no entries" |
| TKEYS005 | Info | Key name starts with lowercase | "Key 'welcomeMsg' should use PascalCase" |