FmgLib.MauiMarkup includes a lightweight localization system fed by JSON files, with live language switching — bound texts update instantly when the culture changes, no page reload required.
The recommended form is the options overload — it cannot confuse a file name with a culture name, and it is where the fallback culture, missing-key policy and culture-sync mode live:
C#
builder.UseMauiMarkupLocalization(o => o
.UseFiles("Common.json", "Checkout.json") // merged in order; later files win on duplicate keys
.UseDefaultCulture("en-US") // startup language
.UseFallbackCulture("en-US")); // used when the current culture yields nothing
Shorter forms:
C#
// default: looks for "Localization.json" in the app package
.UseMauiMarkupLocalization()
// set the startup language
.UseMauiMarkupLocalization(defaultLang: "en-US")
// startup language + custom files
.UseMauiMarkupLocalization(defaultLang: "en-US", "Loc1.json", "Loc2.json")
// files only — the argument MUST be named, because the first positional parameter is the culture
.UseMauiMarkupLocalization(filePaths: new[] { "Localization1.json", "/Languages/Temp1.json" })
Watch the first argument. In UseMauiMarkupLocalization(defaultLang, params filePaths) the culture comes first, so UseMauiMarkupLocalization("Common.json", "Checkout.json") passes a file name as the culture. That is now rejected at startup with a message naming the fix — use filePaths: or the options overload.
Loading is synchronous and throws: a missing or malformed language file fails the app at startup rather than leaving every label showing its raw key.
Every property bound with Translate updates immediately (the translator implements INotifyPropertyChanged and the bindings listen to it). ChangeCulture is safe to call from a background thread — the notification is marshalled to the main thread for you.
By default a culture change also sets CultureInfo.DefaultThreadCurrentCulture and DefaultThreadCurrentUICulture, so dates, numbers and currency follow the selected language too. Narrow or disable it if your app formats persisted values with the ambient culture:
These return a snapshot.new Label().Text("Hello".ToTranslate()) compiles and shows the right text, but it will not update when the language changes — there is no binding behind it. For anything on screen use .Text(e => e.Translate("Hello")).
A translated sentence usually carries a runtime value. TranslateFormat binds the translation and the arguments, so the label re-renders when the language changes and when any argument changes:
Argument paths resolve against the element's BindingContext. Placeholders are formatted with the selected culture, so {1:C} renders $1,234.50 in en-US and 1.234,50 ₺ in tr-TR. If a translation loses its {0} the label falls back to the raw pattern instead of throwing.
By default a key with no translation renders as the key itself. Pick a different policy when that is not what you want:
C#
builder.UseMauiMarkupLocalization(o => o
.UseFiles("Localization.json")
.OnMissingTranslation(MissingTranslationBehavior.Marker)); // renders ⟦Key⟧ — impossible to miss
Behaviour
Result for a missing Hello
ReturnKey(default)
Hello
ReturnEmpty
(empty)
Marker
⟦Hello⟧
Throw
KeyNotFoundException
The RESX translator honours the same setting, so switching backend does not switch behaviour.
Split by feature:UseMauiMarkupLocalization(o => o.UseFiles("Common.json", "Checkout.json", "Settings.json")). Files are merged into one dictionary; on duplicate keys, later files override earlier ones — per language, so a feature file can override one language of a key without repeating the others.
Missing keys: prefer meaningful key names ("Login_InvalidPassword"), and consider MissingTranslationBehavior.Marker in Debug builds so gaps are visible on screen.
Culture fallback: a lookup walks tr-TR → tr → the configured FallbackCulture. Writing shared keys under the neutral language ("tr", "en") covers every regional variant at once.
If a language file is missing or malformed, startup throws a FileLoadException describing the expected format — validate files as part of CI.