Runic.Translations.Wpf 0.8.0

dotnet add package Runic.Translations.Wpf --version 0.8.0
                    
NuGet\Install-Package Runic.Translations.Wpf -Version 0.8.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="Runic.Translations.Wpf" Version="0.8.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Runic.Translations.Wpf" Version="0.8.0" />
                    
Directory.Packages.props
<PackageReference Include="Runic.Translations.Wpf" />
                    
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 Runic.Translations.Wpf --version 0.8.0
                    
#r "nuget: Runic.Translations.Wpf, 0.8.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 Runic.Translations.Wpf@0.8.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=Runic.Translations.Wpf&version=0.8.0
                    
Install as a Cake Addin
#tool nuget:?package=Runic.Translations.Wpf&version=0.8.0
                    
Install as a Cake Tool

Runic.Translations.Wpf

The bounded native RMF2 adapter targets WPF on .NET 10 for Windows. Construct a WpfInlineRenderer from the generated markup contract once, then call SetContent on a TextBlock on its UI thread. The caller supplies an Action<Uri> navigation handler, typed action/link/icon slots, optional custom markup factories, and an optional theme callback. Rendering never invokes navigation or action callbacks.

With the readable generated surface, bind a structured message's slots through its typed slot type and pass the bound value. Use named arguments, because slots of the same kind share a type:

var renderer = new WpfInlineRenderer(AppTextCatalog.Rmf2MarkupContract, Navigate);
renderer.SetContent(helpBlock, text.Messages.checkout_help.Bind(new(
    guide: new InlineLinkBinding(guideUri),
    retry: new InlineActionBinding(Retry))));

A wrong binding kind, a missing or misspelled slot, or another message's slot type is a compile error. The renderer still validates every runtime rule (slot multiplicity, link schemes, icon alternate text, nesting) against the current locale's content, including external packs. The string-key overload SetContent(target, key, content, slots) remains for dynamic keys. Build one renderer per catalog from that catalog's Rmf2MarkupContract: a bound value from another catalog is detected only at runtime, by an unknown key or mismatched slots, or not at all when that catalog has a compatible message with the same key.

The constructors that take the contract JSON link it on every call. When an inline and a document renderer (or several renderers) share a catalog, link the contract once and pass the Rmf2MarkupContract; custom factories and the theme go in an options object:

Rmf2MarkupContract contract = Rmf2MarkupContract.Link(AppTextCatalog.Rmf2MarkupContract);
var renderer = new WpfInlineRenderer(contract, Navigate,
    new WpfInlineRendererOptions { Custom = factories, Theme = ApplyTheme });
var documents = new WpfDocumentRenderer(contract, Navigate) { HeadingBase = 2 };

WpfDocumentRendererOptions carries the document renderer's factories and TextElement theme. A linked contract is immutable and can be shared by any number of renderers.

Icons use InlineIconBinding with a Func<FrameworkElement> that creates a fresh asset for every render. Meaningful icons require a localized accessible name; decorative icons are excluded from WPF's control/content accessibility views. Custom factories receive validated options and child inlines; they must return a fresh Inline. They are linked by canonical contract name, such as shop:badge.

WPF controls are not shared between messages or windows. Replacing content drops the previous inline tree and deactivates its callbacks, even if a caller retains a detached button. Call ClearContent when disposing the host. The adapter builds the replacement before touching the existing control so a binding failure preserves its content. The theme owns final typography and button presentation. No icon library ships.

Retired links and action buttons are also disabled, so retained detached controls look inert.

WPF quick start

Localize one screen with Runic Translations while the rest of the application keeps its .resx and ResourceDictionary resources. WPF on .NET 10 for Windows.

1. Install

Your WPF project targets Windows:

<PropertyGroup>
  <TargetFramework>net10.0-windows</TargetFramework>
  <UseWPF>true</UseWPF>
</PropertyGroup>

Add the runtime and the build package, or reference the translations library that holds your catalog (see the .NET quick start). Use one exact version for all of them:

dotnet add package Runic.Translations.Wpf --version <VERSION>
dotnet add package Runic.Translations.Build --version <VERSION>

Runic.Translations.Wpf brings Runic.Translations with it.

2. Author a catalog

Put runic.json and one .rmf2 file per locale in the project (the template scaffolds them). For one screen:

orders {
  title = Your orders
  greeting = Hello {$name}
  count =
    .input {$count :integer}
    .match $count
    one {{One open order}}
    * {{{$count} open orders}}
  help = Need help? {#link ref=guide}Read the guide{/link}.
}

Add de.rmf2 (or any locale) with the same keys. The build generates AppText and AppTextCatalog. AppText.Messages is the readable surface, with members named by the flattened key: orders_title, orders_greeting(name), orders_count(count) and orders_help, whose slots type is AppTextSlots.orders_help.

3. Create one source at startup

protected override async void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);
    ITranslationManager manager = await AppTextCatalog.CreateManagerAsync("en");
    var text = new AppText(manager);
    var renderer = new WpfInlineRenderer(AppTextCatalog.Rmf2MarkupContract, uri => Process.Start(
        new ProcessStartInfo(uri.AbsoluteUri) { UseShellExecute = true }));
    TranslationSource.Default = new TranslationSource(manager, text.Messages, renderer);
    new MainWindow { DataContext = new MainViewModel(manager) }.Show();
}

Pass text.Messages: the constructor takes the generated readable surface (IReadableMessages), so passing text does not compile. Create the source on the UI thread (it uses the application's dispatcher, else the current thread's, and throws if there is none; pass a Dispatcher otherwise). The renderer is needed only for messages with markup. Set Default once at startup: replacing it after a binding used it throws, and disposing the default source clears it. Bindings that are already loaded keep the source they resolved, so after a reset or dispose they stop following the new default; reload those views or name a Source explicitly.

If you await in OnStartup, keep the default context: ConfigureAwait(false) would continue on a thread-pool thread, where the source cannot find a dispatcher and Show() fails.

Using StartupUri in App.xaml? WPF loads that window right after OnStartup returns, which is before an awaited CreateManagerAsync has finished. The window would then show [key] texts and skip the load check, and its bindings do not pick up the source once it exists. Create the manager synchronously instead. The embedded catalog needs no I/O, so CreateManager returns at once:

protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);
    ITranslationManager manager = AppTextCatalog.CreateManager("en");
    TranslationSource.Default = new TranslationSource(manager, new AppText(manager).Messages, renderer);
}

Alternatively, remove StartupUri and show the window yourself after the await, as above. External catalogs (CreateExternalManagerAsync) load packs and stay asynchronous.

4. Bind the screen

<Window xmlns:rt="https://runic-artifex.eu/xaml/translations"
        xmlns:res="clr-namespace:MyApp.Properties" ...>
  <StackPanel>
    
    <TextBlock Text="{rt:Message orders_title}" FontSize="20"/>

    
    <TextBlock Text="{rt:Message orders_greeting, Arg0={Binding UserName}}"/>
    <TextBlock Text="{rt:Message orders_count, Arg0={Binding OpenOrders}}"/>

    
    <TextBlock rt:TranslationProperties.RichMessage="orders_help"
               rt:TranslationProperties.Slots="{Binding HelpSlots}"/>

    
    <Button Content="{x:Static res:Resources.Close}"/>
  </StackPanel>
</Window>

The https://runic-artifex.eu/xaml/translations namespace (prefix rt) maps to Runic.Translations.Wpf; the CLR form clr-namespace:Runic.Translations.Wpf;assembly=Runic.Translations.Wpf still works.

{rt:Message} works on any dependency property (Content, Header, ToolTip, Title, ...), in styles and in templates. The slots are the generated typed object, so a wrong binding kind or a missing slot is a compile error in the view model:

public AppTextSlots.orders_help HelpSlots { get; } =
    new(guide: new InlineLinkBinding(guideUri));

An IReadOnlyDictionary<string, InlineMarkupBinding> by slot ID is accepted for dynamic use, without compile-time checks.

With two or more inputs, use the long form. Arg0..Arg3 follow the generated method's parameters, which are sorted by input name rather than by their order in the text, so for Editing {$name} ({$email}) Arg0 is email; the build warns (RTR0084) when several inputs are bound by position. The long form binds by name. The names are the generated method's parameter names:

<TextBlock>
  <TextBlock.Text>
    <rt:Message Key="orders_greeting">
      <rt:MessageInput Name="name" Value="{Binding UserName}"/>
    </rt:Message>
  </TextBlock.Text>
</TextBlock>

For rich messages with inputs, set rt:TranslationProperties.Arguments to a list in parameter order or a dictionary by parameter name. A null input becomes empty text; other values convert with the invariant culture to the parameter type, and a number input that is null or not convertible is an error.

Mistakes surface early. A gap in Arg0..Arg3 or mixed input forms fails when the XAML loads. With an explicit Source= (or, for a rich message, a rt:TranslationProperties.Source set on the element before RichMessage), a mistyped message name, a wrong input name or count, or using a plain message as rich (or the reverse) fails then too, with the parameter names in the exception. Without one, the catalog is inherited or the default (see step 6) and cannot be known at load, so the load asks every live TranslationSource: it fails when none has a matching message, which catches a typo in a one-catalog app, while a key that exists only in a parent's catalog still loads. The exact check, and everything found while binding or rendering (a missing slot, an input of the wrong type, a throwing message), is traced like any WPF binding error: plain text shows [key], rich content keeps what it had. With no source created yet (designers, early startup) nothing is checked at load. A rich message whose inputs have not been set yet renders once they are.

5. Switch locale

await manager.SetLocaleAsync("de");

Every {rt:Message} and RichMessage refreshes when the manager publishes the new snapshot, from any thread: the source marshals to the dispatcher it was created on, and each rich element renders on its own dispatcher. Several changes in a row render once. The source follows ITranslationManager.SnapshotPublished, which a manager raises for a locale switch and for RefreshAsync (for example after external pack bytes change), although a refresh never raises LocaleChanged.

Nothing leaks when windows close: the manager holds only a weak reference to the source, the source holds rich elements weakly, and WPF's own bindings do the rest. Dispose the source only if you replace it during the application's life.

6. Coexist with existing resources

{rt:Message} is an ordinary markup extension, so it mixes with {x:Static}, {DynamicResource} and {StaticResource} in one tree, and you can move a screen at a time. Keep .resx for strings you have not migrated. To use a second catalog, create another TranslationSource and set it on a parent element with rt:TranslationProperties.Source, for example in XAML with {x:Static} or a resource. Every {rt:Message} and RichMessage below that element, including those in templates instantiated under it, uses that catalog even when the other catalog has the same key; elsewhere TranslationSource.Default applies. A single {rt:Message Source=...} overrides both. Style setters follow the styled element, like any other element. Locale and culture are separate: the source follows the manager's locale only, so set CultureInfo and FrameworkElement.Language yourself where existing resources need them. There is no .resx importer and no FlowDocument conversion; use WpfDocumentRenderer for document messages.

Create every TranslationSource before loading the views that use it (in OnStartup, before InitializeComponent). A message that only a source created later has fails the load with "No live TranslationSource has a message ...", because the load check cannot know about a source that does not exist yet; create the source earlier or pass Source= explicitly.

Designer

Visual Studio and Blend do not run OnStartup, so no default source exists. {rt:Message key} then shows [key] and rich elements stay empty instead of failing every view. At run time an element without any source traces a binding error and shows [key].

Build-time checks

Set RunicTranslationsXamlCatalog to the catalog ID of the local generating catalog, or of a directly referenced project's catalog (for example translations beside the ViewModels in a class library), to check static XAML keys, plain/rich usage, input counts and readable parameter names during the build. Files with explicit source declarations require a per-file RunicTranslationXaml catalog assertion. A file whose source is inherited from another file or set in code is checked against the default; skip it with <Page Update="Views/OrderSummary.xaml" RunicTranslationsValidateXaml="false" />. Runtime-valued keys and external catalogs keep runtime checks. See build-time XAML checks.

Documents

WpfDocumentRenderer renders RMF2 document messages (paragraphs, headings and flat lists) into a read-only FlowDocumentScrollViewer:

var documents = new WpfDocumentRenderer(AppTextCatalog.Rmf2MarkupContract, Navigate) { HeadingBase = 2 };
documents.SetContent(helpViewer, text.Messages.guide_backup(fileName: fileName).Bind(new(
    check: new InlineActionBinding(Check),
    guide: new InlineLinkBinding(guideUri))));

Headings are bold paragraphs that UI Automation exposes as headings at HeadingBase + level - 1; lists and list items are exposed as UI Automation lists and items with their positions. Links and actions get their slot name as AutomationId. Every render builds a fresh FlowDocument with the content locale's Language and FlowDirection; replacing it or calling ClearContent retires its callbacks. Paragraphs and headings with no children or only empty text, and lists with no items, are skipped. The document block set is open: a later minor release may add blocks, and the renderer fails on a block name it does not know, so update the adapter with the compiler. Copying a selection puts the plain-text projection on the clipboard, with list markers and block breaks, action labels and meaningful icon text, Windows line endings, and without link destinations or rich formats. See the document adapter guide.

The repository has an interactive Windows sample: a window with the bounded example of issue #15 in a FlowDocumentScrollViewer, buttons to render again, clear the document and show the clipboard text. Its source is examples/wpf/document-sample. In a clone of the repository, run it with:

dotnet run --project examples/wpf/document-sample

Cross-compilation is supported through EnableWindowsTargeting. Runtime and accessibility verification require Windows; Linux cannot execute WPF controls.

Product Compatible and additional computed target framework versions.
.NET net10.0-windows7.0 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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.8.0 218 10/22/2026
0.7.0-preview.4 300 10/17/2026
0.7.0-preview.3 147 10/17/2026
0.7.0-preview.2 142 10/15/2026
0.7.0-preview.1 113 10/14/2026
0.6.0-preview.7 56 10/12/2026
0.6.0-preview.5 495 10/9/2026
0.6.0-preview.4 237 10/8/2026
0.6.0-preview.3 314 10/7/2026
0.6.0-preview.2 377 10/7/2026
0.6.0-preview.1 334 10/5/2026
0.5.0-preview.3 63 10/1/2026
0.5.0-preview.2 75 9/25/2026
0.4.0-preview.1 73 9/22/2026