Get started with Runic Translations

On this page

Runic Translations compiles your message files into typed C# and TypeScript APIs. This page explains the concepts and leads to the quick start for your platform.

It covers release 0.7.0-preview.4. The Translations repository owns the detailed guides; every link to it points at that release. Translations is versioned on its own, so its version does not need to match the Application SDK. Use one exact Translations version for every Translations package and tool in a project.

How it works #

You write messages in plain text files, one per locale. A compiler checks them and generates code: a typed C# class for .NET and ES modules with type declarations for the web. Your application calls generated members such as text.Messages.application_title or m.application_title(), so a missing message or a wrong input is a build error, not a blank label at run time. At run time a translation manager holds the current locale and switches it as one step.

The compiler is the same .NET tool everywhere. MSBuild runs it for .NET projects, and a Vite plugin runs it for web projects, so both sides read the same files and agree on every message.

Project layout #

A translations project is a translations/ directory with one configuration file and one message file per locale:

translations/
├── runic.json
├── en.rmf2
└── de.rmf2

runic.json names the catalog, the generated C# class and the locales. This is the configuration from the Vite quick start:

{
  "schemaVersion": 1,
  "catalog": "app",
  "code": { "namespace": "Example", "className": "AppText" },
  "baseLocale": "en",
  "locales": ["en", "de"]
}
  • catalog names the catalog. Web code imports it as virtual:runic-translations/app.
  • code sets the namespace and class name of the generated C#.
  • baseLocale is the locale that defines every message and its inputs.

Message files #

Message files are named {locale}.rmf2. RMF2 groups messages into named blocks; each message body is a MessageFormat 2 pattern. This example comes from the SvelteKit quick start:

home {
  title = Welcome to Runic
  greeting = Hello, {$name}!
}

The German file has the same keys with German text. Groups join their names with an underscore, so home.greeting becomes home_greeting in code. {$name} is an input that the caller must pass. A message can also choose text by a value, for example for plurals:

checkout {
  items =
    .input {$count :integer}
    .match $count
    one {{One item}}
    * {{{$count} items}}
}

The base locale decides which inputs a message has. A translation may use fewer of them or select on them differently, but it cannot add new ones. Messages may also contain markup for links, actions and icons, which the application binds to typed slots. Runic implements a bounded profile of MessageFormat 2, not every Unicode MF2 feature; the RMF2 guide lists what is supported.

What gets generated #

For C#, the source generator in Runic.Translations.Build creates two types during dotnet build. With className set to AppText they are:

  • AppTextCatalog, which creates a translation manager with CreateManagerAsync.
  • AppText, whose Messages property is the readable surface. A message without inputs is a property, such as text.Messages.home_title; a message with inputs is a method, such as text.Messages.home_greeting(name).

The generated C# is not a file you copy or commit. It needs no reflection and works with NativeAOT.

For TypeScript and JavaScript, the compiler emits an ES module package. Its m namespace has one function per message, with type declarations for the exact keys and inputs. The runtime module exports the catalog's locales, baseLocale and a Locale type. A server module adds request-scoped locales for server rendering. With the Vite plugin you import these modules as virtual:runic-translations/app, /runtime and /server, and the plugin writes their declarations to .runic/translations/virtual.d.ts.

Use it in an application #

In .NET, create the manager once at startup and read messages through the generated class. This example comes from the Runic.Translations package:

using Example.Translations;
using Runic.Translations;

ITranslationManager manager = await AppTextCatalog.CreateManagerAsync(
    initialLocale: "en");
var text = new AppText(manager);

Console.WriteLine(text.Messages.application_title);

await manager.SetLocaleAsync("de");
Console.WriteLine(text.Messages.application_title);

Share one manager and one AppText across the application, for example as singletons in your service container, so every window uses the same locale. In a Runic application, ViewModels that show translated text refresh it when the locale changes; see translations in ViewModels.

C# code outside the Views, such as a tray menu, can use the frontend's catalog through a TranslationProject item, also outside the project's directory. The generated catalog now loads each locale on first use. See the Runic Desktop note.

On the web, import m and call a message function:

import { m } from 'virtual:runic-translations/app';

m.application_title();
m.greeting({ name: 'Ada' }, { locale: 'de' });

Switching locales #

In .NET, ITranslationManager.SetLocaleAsync replaces the whole catalog snapshot at once, so a reader never sees half of one locale and half of another. The manager raises LocaleChanged after a switch, and, since 0.6.0-preview.5, SnapshotPublished after every new snapshot, including refreshes.

To follow the user's display language, pass CultureInfo.CurrentUICulture.Name. Since 0.7.0-preview.2, an empty locale, the name of the invariant culture that LANG=C selects, follows the catalog's unsupportedLocale policy like any locale the catalog lacks: it selects the base locale, or throws only with exact. Regenerate ESM output to get the same behavior on the web.

On the web, the runtime's createLocaleSource creates a locale state with getLocale, setLocale and subscribe, and the { locale } option overrides the locale for one call. On a server, runWithLocale gives each request its own locale, so concurrent requests never share it. The Svelte and SvelteKit packages connect these to components and URL routing.

useMessageLocale makes m.*() calls in a Svelte app without SvelteKit follow a locale it owns, such as a ViewModel's setting, without {#key locale}. Call it once in the root component:

<script lang="ts">
  import { useMessageLocale } from '@runic-artifex/translations-svelte/translations';
  import { configureLocaleResolver } from 'virtual:runic-translations/app/runtime';

  let locale = $state('en'); // or a ViewModel's state, such as settings.state?.locale
  useMessageLocale(configureLocaleResolver, () => locale);
</script>

See the Vite quick start.

Validate your messages #

dotnet build validates the catalog and fails on errors. For CI or a web project, the dotnet-runic-translations tool validates without building:

dotnet tool run runic-translations -- validate --project translations

The tool also runs generate to write output and verify to check that committed output is current. Exit code 0 means success, 1 means catalog or verification errors, and 2 means an invalid invocation or an operational failure. Errors carry RTR IDs, which are the same in the build, the tool, the Runic MF2 Translations extensions for VS Code and Visual Studio, and the Editor; the diagnostics reference explains each one.

runic-translations import-text turns tables keyed by their base text into one RMF2 group with stable generated keys:

runic-translations import-text --project translations --group stored \
  --base locales/untranslated.json --table de=locales/de.json

See the RMF2 guide.

The Translations Editor #

The Translations Editor is a desktop app for translators. It opens a translations/ project, shows locale coverage, edits and previews messages with the same compiler, and exports and imports XLIFF. It is available from source only: this preview has no Editor download. See the Editor README to build and launch it.

Choose your platform #

Each quick start starts from an empty folder and ends with a running app:

  • .NET: the .NET quick start creates a translations library with dotnet new runic-translations-project and prints the first message from a console app.
  • WPF: the WPF quick start binds messages in XAML with {rt:Message}, next to existing .resx resources.
  • Vite and TypeScript: the Vite quick start adds the Vite plugin, renders a message and builds on CI.
  • SvelteKit: the SvelteKit quick start adds localized routes, a language switcher and server rendering per request.
  • Command line: translate a CLI with the Runic.Translations.CommandLine adapter, which translates Runic Command Line help, errors and faults.
  • Runic Desktop: there is no separate adapter. Use the .NET runtime and the Vite plugin as above; the Runic Desktop note explains how locale files are delivered.

Next steps #