最后更新:2026年6月19日
.NET MAUI 国际化(i18n)与本地化(l10n)的核心做法是:把所有面向用户的字符串放进 .resx 资源文件,通过 ResourceManager 在运行时按 CultureInfo.CurrentUICulture 解析,并配合 FlowDirection 处理阿拉伯语、希伯来语等 RTL 语言的布局镜像。本指南面向 .NET MAUI 9/10,覆盖资源文件结构、XAML 数据绑定、运行时语言切换、平台特定字符串(Android strings.xml、iOS InfoPlist.strings),以及应用商店元数据本地化的完整工程实践。说实话,这套方案我在上线了 12+ 语言的几款 MAUI 应用里反复验证过,踩过的坑也基本都写进了第 10 节。
- 使用
.resx 默认资源加 AppResources.zh-CN.resx 等区域文件作为字符串单一来源,通过强类型生成器在 XAML 中绑定。
- 在
MauiProgram.cs 中读取系统语言或用户偏好,赋值给 CultureInfo.DefaultThreadCurrentUICulture,确保启动前全局生效。
- RTL 支持只需把根页面的
FlowDirection 设为 MatchParent,并通过 App.Current.MainPage.FlowDirection 动态切换。
- 运行时切换语言要靠
INotifyPropertyChanged 重建 UI,最干净的写法是通过自定义的 LocalizationManager 触发刷新。
- 平台原生字符串(启动画面、权限说明、应用名)必须放进 Android
Resources/values-xx/strings.xml 和 iOS xx.lproj/InfoPlist.strings,.resx 覆盖不到这些场景。
- App Store 与 Google Play 元数据本地化通过 fastlane
metadata/<locale> 目录提交,与代码内本地化解耦但同样关键。
为什么 .NET MAUI 应用必须做本地化
移动应用面对的不是单一市场。根据 Sensor Tower 与 data.ai 2026 年初的报告,亚太与中东地区合计贡献了全球移动应用下载量的 57%,而英语用户仅占 25%。如果你的 .NET MAUI 应用只支持英文,等于在产品里写死了一个对 75% 潜在用户不友好的硬约束。
从产品质量角度看,国际化不仅意味着翻译字符串,它包括区域文化适配(CultureInfo)、双向文本布局(RTL/LTR)、日期与数字格式、货币符号、右键操作与阅读顺序等多个维度。任何一项缺失,都可能在某些市场被用户标记为"不够专业"。
在工程上,本地化越晚做代价越高。当你的代码库已经有几千个硬编码的 Label.Text="提交",再回头抽取到资源文件会触发大量回归测试。我自己第一次接手一个上线两年的 MAUI 项目时,光抽字符串就花了三周。所以我的建议很直接:在项目第一周就把 i18n 脚手架搭好,即使初期只有一种语言,所有字符串也走资源文件管道。这样后续新增任何语言只是"加一份 resx 文件",而不是"重构整个 UI 层"。
如果你之前阅读过我们的.NET MAUI 无障碍开发完全指南,会发现两者在工程哲学上高度一致:都强调"用户面前的每个字符串都要受平台 API 控制"。
第 1 步:创建 .resx 资源文件并启用强类型生成
.NET MAUI 沿用经典的 .NET 资源体系。在共享项目根目录新建 Resources/Strings/ 文件夹,按如下命名规则组织:
AppResources.resx:默认资源(通常为英文或开发者母语)
AppResources.zh-CN.resx:简体中文
AppResources.zh-TW.resx:繁体中文
AppResources.ar.resx:阿拉伯语(RTL)
AppResources.ja.resx:日语
默认 AppResources.resx 的"自定义工具"必须设为 PublicResXFileCodeGenerator(在 Visual Studio 文件属性里),这会生成强类型 AppResources 类,让 XAML 与 C# 都能以 AppResources.WelcomeMessage 这种编译期检查的方式访问字符串。注意,区域文件(zh-CN、ja 等)不要设置自定义工具,它们只是"译文容器",由运行时的 ResourceManager 根据当前文化自动回退到默认文件。
<!-- AppResources.resx 部分 XML -->
<data name="WelcomeMessage" xml:space="preserve">
<value>Welcome to our app</value>
</data>
<data name="SubmitButton" xml:space="preserve">
<value>Submit</value>
</data>
<data name="ItemsCountFormat" xml:space="preserve">
<value>You have {0} items</value>
</data>
第 2 步:在 XAML 中绑定本地化字符串
.NET MAUI 推荐使用 x:Static 标记扩展直接引用强类型资源,这是性能最好、编译期可验证的方案。在页面或 App.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"
x:Class="MyApp.MainPage">
<VerticalStackLayout Padding="20" Spacing="12">
<Label Text="{x:Static strings:AppResources.WelcomeMessage}"
FontSize="24" />
<Button Text="{x:Static strings:AppResources.SubmitButton}"
Clicked="OnSubmitClicked" />
</VerticalStackLayout>
</ContentPage>
但 x:Static 有一个硬限制:它在 XAML 加载时一次性求值,无法响应运行时语言切换。如果你需要让用户在应用内切换语言并立即看到变化(参见第 4 步),就得改用 MarkupExtension 配合 INotifyPropertyChanged。下面是一个生产可用的 TranslateExtension:
using System.Globalization;
using System.Resources;
using MyApp.Resources.Strings;
[ContentProperty(nameof(Key))]
public class TranslateExtension : IMarkupExtension<BindingBase>
{
public string Key { get; set; } = string.Empty;
public BindingBase ProvideValue(IServiceProvider serviceProvider)
{
return new Binding
{
Mode = BindingMode.OneWay,
Path = $"[{Key}]",
Source = LocalizationManager.Instance
};
}
object IMarkupExtension.ProvideValue(IServiceProvider sp) => ProvideValue(sp);
}
public sealed class LocalizationManager : INotifyPropertyChanged
{
public static LocalizationManager Instance { get; } = new();
private readonly ResourceManager _rm =
new("MyApp.Resources.Strings.AppResources", typeof(AppResources).Assembly);
public string this[string key] =>
_rm.GetString(key, CultureInfo.CurrentUICulture) ?? $"!{key}!";
public event PropertyChangedEventHandler? PropertyChanged;
public void Invalidate() => PropertyChanged?.Invoke(this, new(null));
}
XAML 中使用 Text="{local:Translate WelcomeMessage}" 即可。当语言切换时调用 LocalizationManager.Instance.Invalidate(),所有绑定会同时刷新。
第 3 步:启动时设置 CultureInfo
必须在 App 构造函数甚至更早的 MauiProgram 阶段就把 CultureInfo 设定好,否则首屏会闪烁默认语言。推荐流程是:读取用户偏好,回退到系统语言,最后设置全局默认。
// MauiProgram.cs
public static MauiApp CreateMauiApp()
{
ApplyCulture();
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>()
.ConfigureFonts(/* ... */);
return builder.Build();
}
private static void ApplyCulture()
{
// 1. 用户偏好(来自 Preferences API,相当于 NSUserDefaults / SharedPreferences)
var userLang = Preferences.Default.Get("app_language", string.Empty);
// 2. 回退到系统语言
var cultureName = !string.IsNullOrWhiteSpace(userLang)
? userLang
: CultureInfo.CurrentCulture.Name;
var culture = new CultureInfo(cultureName);
// 3. 设为整个进程的默认文化
CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = culture;
}
第 4 步:实现运行时语言切换
"重启应用才能切换语言"在 2026 年是绝对不可接受的用户体验。借助第 2 步的 LocalizationManager,运行时切换可以做到 60fps 丝滑:
public partial class SettingsViewModel : ObservableObject
{
[RelayCommand]
private async Task ChangeLanguageAsync(string cultureCode)
{
// 1. 持久化用户选择
Preferences.Default.Set("app_language", cultureCode);
// 2. 立刻应用新文化
var culture = new CultureInfo(cultureCode);
CultureInfo.DefaultThreadCurrentCulture = culture;
CultureInfo.DefaultThreadCurrentUICulture = culture;
// 3. 通知所有绑定刷新
LocalizationManager.Instance.Invalidate();
// 4. 处理 RTL:根据新语言切换 FlowDirection
Application.Current!.MainPage!.FlowDirection =
culture.TextInfo.IsRightToLeft
? FlowDirection.RightToLeft
: FlowDirection.LeftToRight;
}
}
这种方式比"重新创建 Shell"开销小一个数量级。如果你正在使用 CommunityToolkit.Mvvm 加 Shell 导航(推荐做法,详见我们的.NET MAUI 10 MVVM 与依赖注入指南),RelayCommand 让上面的代码无需任何样板。
第 5 步:支持 RTL 语言与 FlowDirection
阿拉伯语、希伯来语、波斯语、乌尔都语是主要的 RTL 语言。MAUI 通过 FlowDirection 属性原生支持双向布局,三个有效值是:
LeftToRight:强制 LTR
RightToLeft:强制 RTL
MatchParent:继承父容器(默认)
推荐做法:根 App.xaml 不设置 FlowDirection(保持 MatchParent,自动跟随系统),页面级别根据 CultureInfo.CurrentUICulture.TextInfo.IsRightToLeft 动态设置。这样 90% 的布局自动镜像(按钮、标签、图标位置全部翻转),仅需手工处理少数例外,比如:
<!-- 进度条、播放控件这类"时间从左往右流动"的元素需要锁定 LTR -->
<ProgressBar FlowDirection="LeftToRight" Progress="0.6" />
<!-- 图片不应被镜像(除非是箭头图标) -->
<Image Source="logo.png" FlowDirection="LeftToRight" />
很多开发者第一次发布多语言应用时才发现一个尴尬的事实:应用名、相机权限弹窗文案、启动画面文字,.resx 全部覆盖不到。这些是操作系统直接读取的原生资源,必须放在平台原生路径里。
Android:strings.xml 与按区域目录
在 MAUI 项目的 Platforms/Android/Resources/ 下创建:
Resources/
├── values/strings.xml (默认)
├── values-zh-rCN/strings.xml (简体中文)
├── values-ar/strings.xml (阿拉伯语)
└── values-ja/strings.xml (日语)
<!-- values-zh-rCN/strings.xml -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">我的应用</string>
</resources>
在 MainApplication.cs 的 [Application] 特性里引用 @string/app_name 而不是硬编码。注意 Android 用 r 前缀表示地区(写成 values-zh-rCN 而非 values-zh-CN),这是历史遗留约定,写错会静默回退到默认值。我自己就被这个坑过一次,调试日志里完全没线索。
iOS:InfoPlist.strings 与 lproj 目录
在 Platforms/iOS/Resources/ 下创建:
Resources/
├── en.lproj/InfoPlist.strings
├── zh-Hans.lproj/InfoPlist.strings
├── ar.lproj/InfoPlist.strings
└── ja.lproj/InfoPlist.strings
/* zh-Hans.lproj/InfoPlist.strings */
"CFBundleDisplayName" = "我的应用";
"NSCameraUsageDescription" = "我们需要访问相机以扫描二维码";
"NSPhotoLibraryUsageDescription" = "选择头像需要访问照片图库";
另外要在 Info.plist 添加 CFBundleLocalizations 数组列出所有支持的语言,否则 App Store 不会把你的应用标记为支持这些语言。Microsoft 官方文档对此有完整说明,可参考 .NET MAUI 本地化官方文档,以及 Apple 的 Xcode Localization 文档 中关于 .lproj 目录的规范说明。
日期、数字与货币的格式化
正确设置 CultureInfo 后,几乎所有 .NET 格式化 API 都会自动按区域规则输出。但有几个需要注意的细节:
var price = 1234567.89m;
// zh-CN: "¥1,234,567.89"
// de-DE: "1.234.567,89 €"
// ar-SA: "1,234,567.89 ر.س."
var formattedPrice = price.ToString("C", CultureInfo.CurrentCulture);
var date = DateTime.Now;
// zh-CN: "2026年6月19日"
// en-US: "Friday, June 19, 2026"
var formattedDate = date.ToString("D", CultureInfo.CurrentCulture);
对于固定格式(比如服务端 API 通信、日志、数据库主键),始终使用 CultureInfo.InvariantCulture,不要让用户区域影响序列化。这是隐藏 Bug 重灾区。我亲眼见过一个团队在土耳其语区域下,因为 "I".ToLower() 返回 "ı" 而不是 "i",导致整个 API 路由匹配失败。.NET 的 .NET 全球化指南 对此有详细论述,强烈建议团队里所有后端工程师都过一遍。
用户在商店看到的应用描述、截图标题、新版本说明也需要本地化。这部分与代码无关,却是发现转化率(CVR)的关键。推荐使用 fastlane 管理:
fastlane/
├── metadata/
│ ├── en-US/
│ │ ├── description.txt
│ │ ├── keywords.txt
│ │ └── release_notes.txt
│ ├── zh-Hans/
│ │ ├── description.txt
│ │ └── ...
│ └── ar-SA/
│ └── ...
└── screenshots/
├── en-US/iPhone-67/
├── zh-Hans/iPhone-67/
└── ar-SA/iPhone-67/
这样在 CI 流程里执行 fastlane deliver 就能一次性把全部语言推送给 App Store Connect 与 Google Play Console。fastlane 官方提供了 App Store 部署完整文档。"代码本地化加元数据本地化"双轨制,是大型团队的标配。
常见陷阱与排查指南
- 切换语言后部分页面不刷新:说明这些页面还在用
x:Static 而非 TranslateExtension。批量替换即可。
- iOS 模拟器修改语言无效:必须在"设置 → 通用 → 语言与地区"中重启应用,仅修改 Scheme 启动参数有时不生效。
- Android 在 zh-HK 下回退到英文:Android 资源限定符是
values-zh-rHK,注意 r 前缀。
- 资源文件构建后未嵌入 dll:检查
.csproj,所有 .resx 应自动作为 EmbeddedResource,若手动改过编译动作需还原。
- 阿拉伯语数字显示为英文:这是
NumberFormatInfo.DigitSubstitution 设置,部分场景需手动设为 National。
- 翻译字符串过长撑破布局:德语常比英文长 30%,QA 时务必跑 pseudo-localization(比如把所有字符串包成
«<original>» 模拟扩展),提前发现截断。
关于 RTL 语言的字符处理细节,Unicode CLDR 项目是权威数据源,包含数百种区域的格式规则、复数形式、列表分隔符等元数据,.NET 内部的 globalization 数据正是来源于此。
常见问题
.NET MAUI 中 i18n 和 l10n 有什么区别?
i18n(国际化)指让应用具备支持多语言、多区域的能力,比如字符串外置、Culture 抽象、RTL 兼容;l10n(本地化)指针对某个具体区域实际添加翻译与资源,比如新增 ar.resx、调整截图。i18n 是一次性架构工作,l10n 是持续运营工作。
.NET MAUI 应用如何在不重启的情况下切换语言?
关键有两点:一是不能用 x:Static(它只在 XAML 加载时求值一次),而要使用 MarkupExtension 返回 Binding,绑定到实现 INotifyPropertyChanged 的 LocalizationManager;二是切换时调用 Invalidate() 触发 PropertyChanged,所有绑定会同时刷新,包括 FlowDirection 切换 RTL/LTR。
为什么我的 .resx 文件改了应用名却不生效?
应用名(启动器图标下方的文字)由操作系统在启动前读取,.resx 还没加载就已经显示。必须改 Android 的 Platforms/Android/Resources/values-xx/strings.xml 里的 app_name,以及 iOS 的 Platforms/iOS/Resources/xx.lproj/InfoPlist.strings 中的 CFBundleDisplayName。
.NET MAUI 支持哪些 RTL 语言?
任何 CultureInfo.TextInfo.IsRightToLeft 返回 true 的区域都受支持,包括阿拉伯语 (ar)、希伯来语 (he)、波斯语 (fa)、乌尔都语 (ur)、库尔德语 (ku) 等。MAUI 通过 FlowDirection="RightToLeft" 自动镜像布局、文本对齐与图标位置,无需手动重写 XAML。
本地化字符串应该让译者直接改 .resx 还是用专门工具?
团队规模超过 1 人就应该用专门的 TMS(Translation Management System),比如 Crowdin、Lokalise、Phrase。译者在网页界面工作,CI 自动同步 .resx,避免 Git 冲突和翻译记忆库丢失。小项目可用 ResX Manager(Visual Studio 扩展)。
如何测试我的 MAUI 应用的本地化是否正确?
三层测试:1) pseudo-localization(自动用方括号和扩展字符替换所有字符串),快速发现硬编码与布局截断;2) 在模拟器/真机切换系统语言跑 E2E 测试;3) 用 Appium 或 Maestro 编写多语言回归脚本,详见我们的 .NET MAUI 测试完全指南。