Localization
FalkForge supports multi-language installers with JSON-based localization and culture fallback chains. Your installer can detect the user's language and display the right strings automatically.
Built-in Cultures
FalkForge ships with two built-in cultures: English (en-US) and Swedish (sv-SE). One method call loads both.
package.Localization(loc =>
{
loc.AddBuiltInCultures();
loc.DefaultCulture("en-US");
loc.DetectCulture();
});
AddBuiltInCultures()loads the bundleden-USandsv-SEstring tables.DefaultCulture("en-US")sets the fallback language. If no match is found, this culture wins.DetectCulture()reads the OS locale at build time and selects the closest match.
Deep dive: Culture fallback chain
When FalkForge resolves a localized string, it walks a fallback chain from most specific to least specific:
- Exact match — e.g.
sv-SE - Parent culture — e.g.
sv(the neutral language without a region) - Default culture — e.g.
en-US
For example, if the OS reports de-AT (Austrian German), the chain is: de-AT → de → en-US. A string defined only in de still appears for Austrian users. A string missing from both falls back to en-US.
Custom Localization Files
Add your own languages with JSON files. Each file is a flat object mapping string IDs to translated text.
// strings.en-US.json
{
"ProductName": "Localized App",
"WelcomeMessage": "Welcome to the installation wizard",
"FeatureCore": "Core Application",
"FeaturePlugins": "Optional Plugins",
"FinishMessage": "Installation complete. Click Finish to exit."
}
Load files with AddJsonFile(). FalkForge infers the culture from the file name. The naming convention is name.culture.json.
var langDir = Path.Combine(AppContext.BaseDirectory, "lang");
loc.AddJsonFile(Path.Combine(langDir, "strings.en-US.json"));
loc.AddJsonFile(Path.Combine(langDir, "strings.de.json"));
loc.AddJsonFile(Path.Combine(langDir, "strings.fr.json"));
Reference localized strings anywhere you use a string property. The syntax is !(loc.StringId).
package.Name = "!(loc.ProductName)";
package.Description = "!(loc.WelcomeMessage)";
At compile time, FalkForge replaces each !(loc.StringId) reference with the resolved string for the selected culture.
Deep dive: String resolution and circular reference detection
The LocalizedStringResolver processes every string property in the package model. When it encounters !(loc.StringId), it looks up the key in the active culture's string table, then walks the fallback chain if needed.
Localized strings can reference other localized strings. For example, "AppTitle" could resolve to "!(loc.ProductName) v2". The resolver follows these references recursively.
To prevent infinite loops, the resolver tracks visited keys during each resolution. If it encounters a key it has already seen, it stops and reports a LOC003 circular reference error instead of hanging.
UI Localization
For bundle installers with a WPF custom UI, FalkForge provides runtime culture switching. Users can change the language without restarting the installer.
loc.AllowLanguageSelection();
This enables a built-in LanguageSelectorControl in the UI. It displays a dropdown of all loaded cultures. When the user picks a different language, the UI updates immediately.
Deep dive: How WPF strings refresh on culture change
FalkForge's UI localization uses WPF DynamicResource bindings. Each localized string is stored as a resource in the application's resource dictionary.
When the user selects a new culture, the UiStringResolver reloads the resource dictionary with strings from the new culture. Because the bindings are dynamic (not static), WPF automatically re-evaluates every bound element.
Custom pages built with InstallerPage can call Localize() for O(1) string lookup. The base class also provides NotifyCultureChanged(), which triggers a blanket WPF property refresh so all bound UI elements pick up the new strings.
The Localization API
Demo 08 ties everything together. Here is the full localization setup from demo/08-localization/Program.cs.
Configure the LocalizationBuilder
package.Localization(loc =>
{
loc.AddBuiltInCultures();
loc.AddJsonFile(Path.Combine(langDir, "strings.en-US.json"));
loc.AddJsonFile(Path.Combine(langDir, "strings.de.json"));
loc.AddJsonFile(Path.Combine(langDir, "strings.fr.json"));
loc.AddCulture("de-AT", new Dictionary<string, string>
{
["FinishMessage"] = "Installation abgeschlossen. Bitte auf Fertigstellen klicken."
});
loc.DefaultCulture("en-US");
});
This loads four cultures: two built-in (en-US, sv-SE), three from JSON (en-US, de, fr), and one inline (de-AT). The Austrian German culture only overrides FinishMessage. Every other string falls back to de, then en-US.
Use localized strings in metadata
package.Name = "!(loc.ProductName)";
package.Description = "!(loc.WelcomeMessage)";
The product name appears as "Localized App" in English, "Lokalisierte App" in German, and "Application Localisee" in French.
Localized feature titles
package.Feature("Core", f =>
{
f.Title = "!(loc.FeatureCore)";
f.Description = "!(loc.FeatureCore)";
f.IsRequired = true;
});
Feature titles in the FeatureTree dialog show the translated text. A German user sees "Kernanwendung" where an English user sees "Core Application".
Localized shortcuts
package.Shortcut("!(loc.ProductName)", "app.exe")
.WithDescription("!(loc.WelcomeMessage)")
.OnStartMenu("Falk Software");
The Start Menu shortcut name and tooltip are localized too. Any string property accepts !(loc.*) references.
Try it
dotnet run --project demo/08-localization
Build the demo and inspect the output MSI. The dialog text, feature names, and shortcut labels all reflect the resolved culture.
Recap
- Always set a
DefaultCulture. It is the final fallback when no other culture matches. - Use the built-in cultures (
en-US,sv-SE) as templates for your own JSON files. - Test with multiple languages. Run the demo, change your OS locale, and verify the output.
- Keep string IDs consistent across all JSON files. A missing key silently falls back — easy to miss.
- Use
AddCulture()for regional variants that only override a few strings.