Lookup modes and culture fallback
Three ways to ask for a string, one fallback story, and one way to find out when a key does not resolve.
The three lookup modes
| Mode | What you pass | When to use it |
|---|---|---|
| Typed | a generated ResourceKey |
Almost always. The key name is compile-checked, and the key carries its own ResourceManager, so the lookup goes straight to one file |
| Scoped | a key name and a ResourceManager |
A key that is not eligible for generation, or a name assembled at run time, in a known file |
| Search-all | a key name | A key that could live in any of several files, and you would rather not say which |
// Typed: the generated key knows which .resx it came from.
Localizer.Current.Get(AppStringsKeys.Greeting);
// Scoped: this key, in this file.
Localizer.Current.Get("Greeting", AppStrings.ResourceManager);
// Search-all: this key, in whichever registered file has it first.
Localizer.Current.Get("Greeting");
The same three modes exist in XAML:
<TextBlock Text="{l:Localize {x:Static res:AppStringsKeys.Greeting}}" />
<TextBlock Text="{l:Localize Key=Greeting, ResourceManager={x:Static res:AppStrings.ResourceManager}}" />
<TextBlock Text="{l:Localize Greeting}" />
Registration is only for search-all
Localizer.Current.RegisterResourceManager(AppStrings.ResourceManager);
Search-all inspects the registered managers in registration order and takes the first hit, so registration order is what decides which file wins when two of them carry the same key name. Typed and scoped lookups need no registration at all — they already name the file.
UnregisterResourceManager and ClearResourceManagers remove managers again, for an application
that loads resources dynamically.
Tip
In Avalonia, put the registration and the initial CurrentCulture inside BuildAvaloniaApp()
rather than Main. BuildAvaloniaApp runs at run time and under the XAML previewer, so
search-all lookups resolve at design time too instead of showing the !Greeting! sentinel.
Culture fallback
Fallback is .NET's own ResourceManager behaviour, unchanged. The localizer sits on top of it.
| Situation | Result | Raises TranslationNotFound |
|---|---|---|
de-DE entry exists |
the de-DE value |
No |
Missing in de-DE, exists in de |
the parent de value |
No |
| Missing in every satellite, exists in the neutral resources | the neutral value | No |
| Missing across the complete fallback chain | !key! by default |
Yes |
Because fallback runs first, a key that resolves from a parent or from the neutral file does not count as missing. The sentinel and the event report keys that cannot be resolved at all — they are not a report of incomplete per-language coverage.
Missing-translation diagnostics
// Change the sentinel, or make it invisible in production.
Localizer.MissingTranslationFormat = "«{0}»";
// Log every unresolvable key.
Localizer.Current.TranslationNotFound += (_, args) => logger.LogWarning("No translation for {Key}", args.Key);
The default sentinel is !key!, which is deliberately ugly: a missing translation should be
noticeable in a screenshot.
Available cultures
foreach (var culture in Localizer.Current.GetAvailableCultures())
{
// The invariant culture stands for the neutral resources compiled into the assembly.
}
GetAvailableCultures() reports the cultures the application actually ships, by asking each
registered resource manager which satellite assemblies exist. It is what a language picker binds to.
Dependency injection
services.AddSingleton<ILocalizer>(_ => Localizer.Current);
Alternatively, assign a container-owned implementation to Localizer.Current before any view is
created. The property rejects null, and the markup extensions always read its current value, so a
replacement is picked up everywhere.
Lifetime and subscriptions
Localizer.Current lives for the whole process, so a strong CultureChanged subscription keeps its
subscriber alive for the whole process too. A short-lived subscriber — a view model owned by a
window, say — should unsubscribe when it is disposed. Both sample view models show the pattern.
The bindings themselves need no care: Avalonia's markup extensions subscribe through weak events, and WPF binds to the singleton through WPF's own weak binding-target references, so a discarded control stays collectable.