Tutorials

Localization

10 min read · Prerequisite: Getting Started · Wraps demo/08-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();
});
Deep dive: Culture fallback chain

When FalkForge resolves a localized string, it walks a fallback chain from most specific to least specific:

  1. Exact match — e.g. sv-SE
  2. Parent culture — e.g. sv (the neutral language without a region)
  3. Default culture — e.g. en-US

For example, if the OS reports de-AT (Austrian German), the chain is: de-ATdeen-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