How to Localize .NET MAUI Apps: Multi-Language Support, Dynamic Switching, and RTL Layouts

Learn how to add multi-language support to your .NET MAUI apps with RESX resource files, dynamic runtime language switching, RTL layout support, and platform-specific setup for iOS, Android, and Windows.

Building a mobile app that only speaks one language? You're leaving users on the table. With over 7,000 languages spoken worldwide and app stores spanning 175+ countries, localization isn't some nice-to-have anymore — it's a genuine competitive advantage. .NET MAUI gives you a solid localization pipeline built on familiar .NET resource files, but honestly, piecing it all together — RESX setup, platform quirks, dynamic language switching, right-to-left layouts — into something that actually works in production can be tricky.

This guide walks you through the entire .NET MAUI localization workflow, from creating your first resource file to shipping a fully multilingual app that switches languages at runtime without restarting. Every code example targets .NET 10 and has been tested on Android, iOS, and Windows.

Why Localization Matters for Mobile Apps

Localization goes beyond translation. It adapts your app's text, formatting, dates, numbers, currency, and layout direction to match the cultural expectations of each audience.

The business case is pretty clear:

  • Wider reach — Apps available in multiple languages see higher download rates in non-English markets
  • Better retention — Users engage more with apps presented in their native language
  • App store visibility — Localized metadata (title, description, keywords) improves discoverability in regional stores
  • Compliance — Some markets and enterprise clients require support for local languages

Understanding the .NET MAUI Localization Pipeline

.NET MAUI inherits the standard .NET localization infrastructure. The core components are:

  • Resource files (.resx) — XML files that store key-value pairs of localized strings
  • ResourceManager — The runtime class that resolves the correct string based on the current culture
  • CultureInfo — Represents a specific language and region combination (e.g., en-US, fr-FR, ar-SA)
  • Satellite assemblies — Compiled resource files that ship alongside your main assembly

When your app requests a localized string, the ResourceManager checks for a culture-specific resource file first, then falls back to the neutral language resource file. This fallback chain ensures your app never displays a blank string — which is a relief, because nothing looks worse to a user than empty labels scattered across the screen.

Step 1: Create Resource Files

Start by organizing your resources. Create a Resources/Strings folder in your .NET MAUI project and add your resource files there.

Default Resource File (AppResources.resx)

This file holds your default (neutral) language strings. Every key you reference in code or XAML must exist here.

<?xml version="1.0" encoding="utf-8"?>
<root>
  <data name="AppTitle" xml:space="preserve">
    <value>My Awesome App</value>
  </data>
  <data name="WelcomeMessage" xml:space="preserve">
    <value>Welcome to our app!</value>
  </data>
  <data name="SettingsTitle" xml:space="preserve">
    <value>Settings</value>
  </data>
  <data name="LanguageLabel" xml:space="preserve">
    <value>Language</value>
  </data>
  <data name="SaveButton" xml:space="preserve">
    <value>Save</value>
  </data>
</root>

Language-Specific Resource Files

Add additional .resx files using the naming convention AppResources.[culture-code].resx. For example:

  • AppResources.fr.resx — French
  • AppResources.de.resx — German
  • AppResources.es.resx — Spanish
  • AppResources.ar.resx — Arabic
  • AppResources.ja.resx — Japanese

Each satellite resource file contains translated strings for the same keys. Set the Access Modifier to No code generation for all non-default resource files — only the default AppResources.resx should generate a backing class. This is a common gotcha that can lead to confusing build errors if you miss it.

Example: AppResources.fr.resx

<data name="AppTitle" xml:space="preserve">
  <value>Mon Application Géniale</value>
</data>
<data name="WelcomeMessage" xml:space="preserve">
  <value>Bienvenue dans notre application !</value>
</data>
<data name="SettingsTitle" xml:space="preserve">
  <value>Paramètres</value>
</data>
<data name="LanguageLabel" xml:space="preserve">
  <value>Langue</value>
</data>
<data name="SaveButton" xml:space="preserve">
  <value>Enregistrer</value>
</data>

Step 2: Set the Neutral Language

Don't skip this step. Without a neutral language declared, the ResourceManager returns null for any culture that doesn't have a matching resource file. Set it in your project file:

<PropertyGroup>
  <TargetFrameworks>net10.0-android;net10.0-ios;net10.0-maccatalyst;net10.0-windows10.0.19041.0</TargetFrameworks>
  <NeutralLanguage>en</NeutralLanguage>
</PropertyGroup>

This tells the resource manager to fall back to AppResources.resx (English) when no culture-specific file matches the user's device language.

Step 3: Access Localized Strings in XAML

Use the x:Static markup extension to bind UI elements to your generated resource class:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:strings="clr-namespace:MyApp.Resources.Strings"
             Title="{x:Static strings:AppResources.SettingsTitle}">

  <VerticalStackLayout Padding="20" Spacing="15">
    <Label Text="{x:Static strings:AppResources.WelcomeMessage}"
           FontSize="24"
           HorizontalOptions="Center" />

    <Label Text="{x:Static strings:AppResources.LanguageLabel}"
           FontSize="16" />

    <Button Text="{x:Static strings:AppResources.SaveButton}"
            Clicked="OnSaveClicked" />
  </VerticalStackLayout>
</ContentPage>

Access Strings in C# Code

You can also grab localized strings programmatically:

using MyApp.Resources.Strings;

string welcomeText = AppResources.WelcomeMessage;
string formatted = string.Format(AppResources.GreetingFormat, userName);

Step 4: Platform-Specific Configuration

Here's where things get platform-specific (and where I've seen a lot of developers get stuck). Each platform has its own requirements for localization to work correctly. Missing these steps is one of the most common causes of localization failures.

iOS and Mac Catalyst

You must declare supported languages in the Info.plist file. Without this, iOS flat-out won't load satellite resource files for non-default languages.

<key>CFBundleLocalizations</key>
<array>
  <string>en</string>
  <string>fr</string>
  <string>de</string>
  <string>es</string>
  <string>ar</string>
  <string>ja</string>
</array>

Android

Android handles localization automatically through the .NET resource system — no additional manifest changes needed. However, for RTL support, make sure your AndroidManifest.xml includes:

<application android:supportsRtl="true" ...>

Good news: this is already included in the default .NET MAUI project template.

Windows

Windows requires explicit language declarations in Package.appxmanifest. Replace the auto-generated resource line:

<!-- Replace this: -->
<Resource Language="x-generate" />

<!-- With explicit languages: -->
<Resource Language="en" />
<Resource Language="fr" />
<Resource Language="de" />
<Resource Language="es" />
<Resource Language="ar" />
<Resource Language="ja" />

For localizing the Windows app display name, create .resw files under Platforms/Windows/Strings/[culture-code]/Resources.resw for each supported language.

Step 5: Dynamic Language Switching at Runtime

This is the feature everyone asks about. Many apps need an in-app language picker that switches the language without requiring a restart. It's also the feature that causes the most headaches, because the default x:Static binding doesn't react to culture changes.

Let's look at three approaches, from simplest to most polished.

Approach 1: Reset the Main Page (Simple)

The simplest approach changes the culture and rebuilds the UI by resetting Application.Current.MainPage:

using System.Globalization;
using MyApp.Resources.Strings;

public static class LocalizationHelper
{
    public static void SwitchLanguage(string cultureCode)
    {
        var culture = new CultureInfo(cultureCode);

        // Update the thread culture
        Thread.CurrentThread.CurrentCulture = culture;
        Thread.CurrentThread.CurrentUICulture = culture;

        // Update the resource manager culture
        AppResources.Culture = culture;

        // Rebuild the UI from scratch
        Application.Current.MainPage = new AppShell();
    }
}

This works reliably across all platforms and is honestly the recommended starting point. The downside? It resets navigation state, so the user ends up on the shell's default page.

Approach 2: Binding-Based Updates (Advanced)

For a seamless experience where the page updates in place, create a localization service that implements INotifyPropertyChanged:

using System.ComponentModel;
using System.Globalization;
using MyApp.Resources.Strings;

public class LocalizationService : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    public static LocalizationService Instance { get; } = new();

    public string this[string key]
        => AppResources.ResourceManager.GetString(key, AppResources.Culture)
           ?? key;

    public void SetCulture(CultureInfo culture)
    {
        Thread.CurrentThread.CurrentCulture = culture;
        Thread.CurrentThread.CurrentUICulture = culture;
        AppResources.Culture = culture;

        // Notify all bindings that every property changed
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(null));
    }
}

Register it in MauiProgram.cs and bind in XAML:

// MauiProgram.cs
builder.Services.AddSingleton(LocalizationService.Instance);

// In XAML:
<Label Text="{Binding [WelcomeMessage], Source={x:Static local:LocalizationService.Instance}}" />

When you call LocalizationService.Instance.SetCulture(new CultureInfo("fr")), every bound label updates immediately without rebuilding the page. It's more work to set up, but the UX is noticeably smoother.

Approach 3: LocalizationResourceManager.Maui Plugin

If you'd rather not roll your own solution, the LocalizationResourceManager.Maui NuGet package provides a ready-made option inspired by the Xamarin Community Toolkit. Install and configure it in MauiProgram.cs:

dotnet add package LocalizationResourceManager.Maui
// MauiProgram.cs
builder
    .UseMauiApp<App>()
    .UseLocalizationResourceManager(settings =>
    {
        settings.AddResource(AppResources.ResourceManager);
        settings.RestoreLatestCulture(true);
    });

Then use the Translate markup extension in XAML:

<ContentPage xmlns:loc="clr-namespace:LocalizationResourceManager.Maui;assembly=LocalizationResourceManager.Maui">

  <Label Text="{loc:Translate WelcomeMessage}" />
  <Button Text="{loc:Translate SaveButton}" />

</ContentPage>

Switch languages by injecting ILocalizationResourceManager and setting its CurrentCulture property. All Translate-bound elements update automatically — no extra plumbing needed.

Step 6: Right-to-Left (RTL) Layout Support

Languages like Arabic, Hebrew, Persian, and Urdu read right to left. .NET MAUI provides automatic RTL support, but there are some important nuances you'll want to handle correctly.

Automatic Flow Direction

.NET MAUI apps automatically respect the device's flow direction based on the selected language. When the device is set to Arabic, for example, the entire layout mirrors — navigation drawers open from the right, text aligns right, and lists scroll in the expected direction. It's pretty seamless when it works.

Manual Flow Direction Override

But what if your in-app language picker changes to Arabic while the device itself is set to English? In that case, you need to set the FlowDirection property explicitly:

// Set on the entire window
Application.Current.Windows[0].Page.FlowDirection = FlowDirection.RightToLeft;

// Or on specific elements
myStackLayout.FlowDirection = FlowDirection.RightToLeft;

// Or match the parent
myLabel.FlowDirection = FlowDirection.MatchParent;

Integrating RTL into the Language Switcher

Here's how to update your language switching method to handle flow direction automatically:

public static void SwitchLanguage(string cultureCode)
{
    var culture = new CultureInfo(cultureCode);

    Thread.CurrentThread.CurrentCulture = culture;
    Thread.CurrentThread.CurrentUICulture = culture;
    AppResources.Culture = culture;

    // Determine flow direction from the culture
    var flowDirection = culture.TextInfo.IsRightToLeft
        ? FlowDirection.RightToLeft
        : FlowDirection.LeftToRight;

    // Rebuild the shell with correct flow direction
    var shell = new AppShell();
    shell.FlowDirection = flowDirection;
    Application.Current.MainPage = shell;
}

RTL Design Tips

  • Mirror your layouts — Swap leading and trailing margins, icons, and navigation arrows
  • Keep code LTR — Code blocks, phone numbers, and email addresses stay left-to-right even in RTL interfaces
  • Bump up font size slightly — Arabic and Hebrew script can appear smaller than Latin text at the same point size; consider increasing by 1-2 points
  • Test with real content — Machine-translated placeholder text often misses layout issues that authentic RTL content reveals

Known iOS RTL Issues

Fair warning: there's a known issue where FlowDirection on iOS may report LeftToRight even when the device language is set to an RTL language. Some controls inside ScrollView and Border can also render incorrectly in RTL mode. Always test on physical iOS devices when supporting RTL — the simulator doesn't always catch these issues.

Step 7: Localizing App Store Metadata

Localization doesn't stop at your app's UI. For maximum discoverability, you'll want to localize your app store listings too:

  • App name — Provide localized app names in each store listing
  • Description — Translate your full app description and feature list
  • Screenshots — Generate localized screenshots showing the app in each language
  • Keywords — Research keywords in each target language separately; direct translations of English keywords often miss high-volume local search terms

On Windows, localize the display name by adding Resources.resw files under Platforms/Windows/Strings/[culture]/.

Step 8: Formatting Dates, Numbers, and Currency

String translation is only part of the puzzle. Dates, numbers, and currency need to adapt to the user's culture too. Thankfully, the .NET globalization APIs handle this automatically when you use the correct format methods:

var culture = CultureInfo.CurrentCulture;

// Date formatting
string date = DateTime.Now.ToString("D", culture);
// en-US: "Wednesday, February 26, 2026"
// de-DE: "Mittwoch, 26. Februar 2026"
// ja-JP: "2026年2月26日木曜日"

// Number formatting
string number = 1234567.89.ToString("N", culture);
// en-US: "1,234,567.89"
// de-DE: "1.234.567,89"
// fr-FR: "1 234 567,89"

// Currency formatting
string price = 49.99m.ToString("C", culture);
// en-US: "$49.99"
// de-DE: "49,99 €"
// ja-JP: "¥50"

Always use CultureInfo.CurrentCulture (not a hardcoded culture) in format calls so the output respects the user's actual settings.

Step 9: Testing Your Localization

Localization bugs are easy to introduce and surprisingly hard to spot. Build testing into your workflow from the start — trust me, it'll save you hours later.

Manual Testing Checklist

  • Change the device language — Verify that every screen displays the correct translations
  • Check string truncation — German and French strings are often 30-40% longer than English; make sure labels, buttons, and navigation titles still fit
  • Test RTL layouts — Set the device to Arabic or Hebrew and walk through every screen
  • Verify fallback behavior — Set the device to an unsupported language and confirm the app falls back to the neutral language gracefully
  • Check date and number formats — Switch between cultures and verify correct formatting

Automated Testing

Write unit tests that verify key strings resolve correctly for each supported culture:

[Theory]
[InlineData("en", "Welcome to our app!")]
[InlineData("fr", "Bienvenue dans notre application !")]
[InlineData("de", "Willkommen in unserer App!")]
public void WelcomeMessage_ReturnsCorrectTranslation(string culture, string expected)
{
    Thread.CurrentThread.CurrentUICulture = new CultureInfo(culture);
    AppResources.Culture = new CultureInfo(culture);

    var result = AppResources.WelcomeMessage;

    Assert.Equal(expected, result);
}

Pseudo-Localization

Before sending strings for professional translation, try pseudo-localization to catch layout issues early. Pseudo-localization replaces each character with an accented version (e.g., "Settings" becomes "Šéttîñgš") and pads strings by 30-40%. This technique is great for revealing truncation, hardcoded strings, and concatenation issues without needing real translations.

Best Practices for .NET MAUI Localization

  1. Never hardcode user-facing strings — Every string shown to the user should come from a resource file, even if you only plan to support one language initially. You'll thank yourself later when that "single-language app" suddenly needs to ship in three more markets.
  2. Use meaningful keys — Name your resource keys semantically (LoginButton_Text, Error_NetworkUnavailable) rather than generically (String1, Label2)
  3. Avoid string concatenation for sentences — Different languages have different word orders; use format strings with placeholders ({0}) instead of concatenating fragments
  4. Handle pluralization carefully — English has two plural forms (one, other), but languages like Arabic have six. Use libraries or format strings that support plural rules.
  5. Keep resource files in sync — Use tooling or CI checks to ensure every key in the default file has a corresponding entry in every satellite file
  6. Test with the longest translations — Design your UI to accommodate longer strings; German text is typically 30% longer than English
  7. Store the user's language preference — Use Preferences.Set("AppLanguage", cultureCode) to persist the user's choice across app restarts
  8. Don't localize code, identifiers, or log messages — Keep these in English for easier debugging and support

Putting It All Together: A Complete Settings Page

So, let's bring everything together. Here's a complete example of a settings page with an in-app language picker that switches languages at runtime:

// SettingsViewModel.cs
using System.Collections.ObjectModel;
using System.Globalization;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MyApp.Resources.Strings;

public partial class SettingsViewModel : ObservableObject
{
    [ObservableProperty]
    private LanguageOption selectedLanguage;

    public ObservableCollection<LanguageOption> Languages { get; } = new()
    {
        new("English", "en"),
        new("Français", "fr"),
        new("Deutsch", "de"),
        new("Español", "es"),
        new("العربية", "ar"),
        new("日本語", "ja")
    };

    public SettingsViewModel()
    {
        var currentCulture = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName;
        SelectedLanguage = Languages.FirstOrDefault(l => l.Code == currentCulture)
                           ?? Languages[0];
    }

    [RelayCommand]
    private void ChangeLanguage()
    {
        var culture = new CultureInfo(SelectedLanguage.Code);

        Thread.CurrentThread.CurrentCulture = culture;
        Thread.CurrentThread.CurrentUICulture = culture;
        AppResources.Culture = culture;

        // Persist the choice
        Preferences.Set("AppLanguage", SelectedLanguage.Code);

        // Set flow direction for RTL languages
        var flowDirection = culture.TextInfo.IsRightToLeft
            ? FlowDirection.RightToLeft
            : FlowDirection.LeftToRight;

        var shell = new AppShell();
        shell.FlowDirection = flowDirection;
        Application.Current.MainPage = shell;
    }
}

public record LanguageOption(string DisplayName, string Code)
{
    public override string ToString() => DisplayName;
}
<!-- SettingsPage.xaml -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:strings="clr-namespace:MyApp.Resources.Strings"
             xmlns:vm="clr-namespace:MyApp.ViewModels"
             x:DataType="vm:SettingsViewModel"
             Title="{x:Static strings:AppResources.SettingsTitle}">

  <VerticalStackLayout Padding="20" Spacing="20">

    <Label Text="{x:Static strings:AppResources.LanguageLabel}"
           FontSize="18"
           FontAttributes="Bold" />

    <Picker ItemsSource="{Binding Languages}"
            SelectedItem="{Binding SelectedLanguage}"
            Title="{x:Static strings:AppResources.LanguageLabel}" />

    <Button Text="{x:Static strings:AppResources.SaveButton}"
            Command="{Binding ChangeLanguageCommand}"
            HorizontalOptions="Fill" />

  </VerticalStackLayout>
</ContentPage>

Restoring the Language on Startup

Don't forget to restore the saved language preference before the UI loads. Add this to your App.xaml.cs:

public partial class App : Application
{
    public App()
    {
        InitializeComponent();

        var savedLanguage = Preferences.Get("AppLanguage", "en");
        var culture = new CultureInfo(savedLanguage);

        Thread.CurrentThread.CurrentCulture = culture;
        Thread.CurrentThread.CurrentUICulture = culture;
        AppResources.Culture = culture;
    }

    protected override Window CreateWindow(IActivationState? activationState)
    {
        var shell = new AppShell();

        if (CultureInfo.CurrentUICulture.TextInfo.IsRightToLeft)
        {
            shell.FlowDirection = FlowDirection.RightToLeft;
        }

        return new Window(shell);
    }
}

Frequently Asked Questions

How do I add a new language to my .NET MAUI app?

Create a new resource file named AppResources.[culture-code].resx in your Resources/Strings folder, translate all keys from the default file, and add the culture code to your iOS Info.plist (CFBundleLocalizations array) and Windows Package.appxmanifest resource declarations. No code changes are needed — the ResourceManager picks up new satellite files automatically.

Can I change the app language at runtime without restarting?

Yes. Update Thread.CurrentThread.CurrentUICulture, set AppResources.Culture, and either reset Application.Current.MainPage for a simple approach, or use a binding-based localization service with INotifyPropertyChanged for in-place updates. The LocalizationResourceManager.Maui NuGet package also provides a ready-made Translate markup extension that handles this for you.

Why are my translations not showing on iOS?

Nine times out of ten, it's a missing CFBundleLocalizations entry in Info.plist. iOS requires explicit language declarations before it loads satellite resource assemblies. Add a string entry for each supported language code in the CFBundleLocalizations array and rebuild.

How do I handle right-to-left languages like Arabic in .NET MAUI?

.NET MAUI automatically mirrors layouts when the device language is RTL. For in-app language switching, set FlowDirection on your root layout or AppShell to FlowDirection.RightToLeft. On Android, make sure android:supportsRtl="true" is in your AndroidManifest.xml (it's included by default in MAUI templates). Test on real devices — iOS has known quirks with RTL rendering inside ScrollView and Border.

Should I use x:Static or a binding-based approach for localization?

Use x:Static if your app follows the device language and doesn't offer an in-app language switcher — it's simpler and has zero runtime overhead. Go with a binding-based approach (custom INotifyPropertyChanged service or the LocalizationResourceManager.Maui plugin) if you need dynamic runtime switching, because x:Static values won't update when the culture changes after the page has already loaded.

About the Author Editorial Team

Our team of expert writers and editors.