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
<PackageReference Include="Runic.Translations.Wpf" Version="0.8.0" />
<PackageVersion Include="Runic.Translations.Wpf" Version="0.8.0" />
<PackageReference Include="Runic.Translations.Wpf" />
paket add Runic.Translations.Wpf --version 0.8.0
#r "nuget: Runic.Translations.Wpf, 0.8.0"
#:package Runic.Translations.Wpf@0.8.0
#addin nuget:?package=Runic.Translations.Wpf&version=0.8.0
#tool nuget:?package=Runic.Translations.Wpf&version=0.8.0
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 | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0-windows7.0 is compatible. |
-
net10.0-windows7.0
- Runic.Translations (>= 0.8.0)
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 |