Skip to content

本地化

Microsoft.UI.Reactor(Reactor)的本地化系统把你的组件树包在一个 LocaleProvider 里,它通过上下文向每个后代提供 IntlAccessor。你可以查找消息、格式化数字与日期、检测 RTL 布局 —— 全部本地化感知、全部响应式。

API 用途
LocaleProvider 用一个活动区域与资源提供程序包裹子树
UseIntl() 返回 IntlAccessor 的 Hook,用于消息、格式化与方向
IStringResourceProvider 可插拔的字符串来源(.resw、内存、自定义)
RtlHelper.IsRtlLocale(tag) 判断从右到左区域设置的静态检查
pseudoLocalize: true LocaleProvider 的开关,为字符串加变音符与填充以用于测试

字符串资源提供程序

先从实现 IStringResourceProvider 开始。它把区域设置、命名空间与键映射到一个已翻译的字符串。生产应用请用 ReswResourceProvider 加载 .resw 文件。演示与测试用一个内存字典即可:

class DemoResourceProvider : IStringResourceProvider
{
    private readonly Dictionary<string, Dictionary<string, string>> _strings = new()
    {
        ["en-US"] = new() {
            ["App.Title"] = "My Application",
            ["App.Greeting"] = "Hello, {name}!",
        },
        ["fr-FR"] = new() {
            ["App.Title"] = "Mon Application",
            ["App.Greeting"] = "Bonjour, {name} !",
        },
        ["ar-SA"] = new() {
            ["App.Title"] = "\u062a\u0637\u0628\u064a\u0642\u064a",
            ["App.Greeting"] = "\u0645\u0631\u062d\u0628\u0627\u060c {name}!",
        }
    };

    public string? GetString(string locale, string ns, string key)
    {
        var fullKey = $"{ns}.{key}";
        return _strings.TryGetValue(locale, out var s)
            && s.TryGetValue(fullKey, out var v) ? v : null;
    }
}

GetString 方法收到完整区域标记(例如 "fr-FR")、命名空间(与 .resw 文件名对应)以及键。缺失的键返回 null —— 系统会自动回退到默认区域。

LocaleProvider

LocaleProvider 包裹你的应用树。它接受一个区域字符串、一个子元素,以及一个可选的资源提供程序:

class LocaleSwitcher : Component
{
    public override Element Render()
    {
        var (localeIndex, setLocaleIndex) = UseState(0);
        var locales = new[] { "en-US", "fr-FR", "ar-SA" };
        var locale = locales[localeIndex];
        var provider = new DemoResourceProvider();

        return VStack(16,
            ComboBox(["English (US)", "Fran\u00e7ais", "\u0627\u0644\u0639\u0631\u0628\u064a\u0629"],
                localeIndex, setLocaleIndex)
                .Header("Locale"),
            LocaleProvider(locale,
                Component<LocalizedContent>(),
                resourceProvider: provider,
                defaultLocale: "en-US")
        ).Padding(24);
    }
}

带 LocaleProvider 的区域切换器

当区域变化时(这里通过 ComboBox),LocaleProvider 会用新的 IntlAccessor 重新渲染其子树。每个调用 UseIntl() 的组件都会自动获得新区域。

消息查找

在任何后代中调用 UseIntl() —— 它是一个 Hook —— 即可拿到 IntlAccessor。用 .Message() 按键查找已翻译的字符串。插值参数以 ("name", value) 元组形式传入 —— 第一项是作为字符串字面量的占位符名(与 .resw 模式中的 {name} 对应),第二项是值。这是元组参数重载支持的紧凑、AOT 安全形态:

class LocalizedContent : Component
{
    public override Element Render()
    {
        var intl = UseIntl();
        var title = intl.Message(new MessageKey("App", "Title"));
        var greeting = intl.Message(
            new MessageKey("App", "Greeting"),
            ("name", "Alice"));

        return VStack(12,
            TextBlock(title).FontSize(24).Bold(),
            TextBlock(greeting).FontSize(16),
            TextBlock($"Locale: {intl.Locale}").Opacity(0.6),
            TextBlock($"Direction: {intl.Direction}").Opacity(0.6)
        );
    }
}

已本地化的消息

MessageKey 接受一个命名空间与键。命名空间对应你的 .resw 文件名(例如 "App" 对应 App.resw)。该访问器还暴露当前的 LocaleDirectionIsRtl 标志。

在以 .resw 为后端的应用里,Reactor.Localization.Generator 在构建时读取默认区域的这些文件,并把同样的键生成为字段(例如 Loc.App.Title)。这些自包含的代码片段会写全 new MessageKey(...),以便内存提供程序依然可见。

格式化数字与日期

IntlAccessor 为数字、日期与列表提供区域感知的格式化。每个方法都返回按当前区域规则格式化后的字符串:

class FormattingDemo : Component
{
    public override Element Render()
    {
        var intl = UseIntl();
        var price = intl.FormatNumber(1234.56,
            new NumberFormatOptions { Style = NumberStyle.Currency });
        var percent = intl.FormatNumber(0.875,
            new NumberFormatOptions { Style = NumberStyle.Percent });
        var date = intl.FormatDate(DateTimeOffset.Now,
            new DateFormatOptions { Style = DateStyle.Long });
        var items = intl.FormatList(
            new[] { "Apples", "Bananas", "Cherries" },
            ListFormatType.Conjunction);

        return VStack(8,
            SubHeading("Formatting"),
            TextBlock($"Price: {price}"),
            TextBlock($"Rate: {percent}"),
            TextBlock($"Date: {date}"),
            TextBlock($"List: {items}")
        ).Padding(24);
    }
}

已格式化的数字与日期

方法 选项
FormatNumber(value, options?) NumberStyle.Default.Currency.Percent;小数位控制
FormatDate(value, options?) DateStyle.Short.Long.Full.Default
FormatList(values, type) ListFormatType.Conjunction("and")或 .Disjunction("or")

格式化遵循区域的约定 —— 小数分隔符、日期顺序、货币符号与列表连接词都会自动适配。

RTL 检测

RtlHelper.IsRtlLocale() 检查某个区域是否从右到左。IntlAccessor 为活动区域暴露 IsRtlDirection

class RtlDemo : Component
{
    public override Element Render()
    {
        var intl = UseIntl();
        var locales = new[] { "en-US", "fr-FR", "ar-SA", "he-IL", "ja-JP" };

        return VStack(8,
            SubHeading("RTL Detection"),
            VStack(4,
                locales.Select(loc =>
                    HStack(8,
                        TextBlock(loc).Width(60),
                        TextBlock(RtlHelper.IsRtlLocale(loc) ? "RTL" : "LTR")
                            .Bold()
                            .Foreground(RtlHelper.IsRtlLocale(loc)
                                ? Theme.SystemCritical : Theme.SystemSuccess)
                    )
                    .WithKey(loc)
                ).ToArray()
            ),
            When(intl.IsRtl, () =>
                TextBlock("Current layout is right-to-left")
                    .Foreground(Theme.SystemCritical).SemiBold())
        ).Padding(24);
    }
}

RTL 检测

阿拉伯语、希伯来语、波斯语、乌尔都语以及其他若干语言都会被检测为 RTL。用 intl.Direction 在你的布局容器上设置 FlowDirection,让文本与 UI 元素正确流向。

伪本地化

伪本地化把字符替换为带变音符的等价字符并加上填充,以暴露硬编码字符串与截断问题。在 LocaleProvider 上设置 pseudoLocalize: true 即可启用:

class PseudoLocDemo : Component
{
    public override Element Render()
    {
        var (pseudo, setPseudo) = UseState(false);
        var provider = new DemoResourceProvider();

        return VStack(12,
            SubHeading("Pseudo-Localization"),
            ToggleSwitch(pseudo, setPseudo,
                header: "Enable pseudo-localization"),
            LocaleProvider("en-US",
                RenderEachTime(ctx =>
                {
                    var intl = ctx.UseIntl();
                    var title = intl.Message(new MessageKey("App", "Title"));
                    var greeting = intl.Message(
                        new MessageKey("App", "Greeting"),
                        ("name", "World"));
                    return VStack(4,
                        TextBlock(title).FontSize(18).Bold(),
                        TextBlock(greeting));
                }),
                resourceProvider: provider,
                pseudoLocalize: pseudo)
        ).Padding(24);
    }
}

伪本地化

在开发期间运行伪本地化可以尽早抓到问题:没有走 intl.Message() 的字符串会原样显示,因而很容易被发现。被填充的文本则暴露出固定宽度布局中的截断。

提示

始终在根部用 LocaleProvider 包裹。 没有提供程序却调用 UseIntl() 的组件会回退到操作系统区域,但你会失去对区域切换与资源加载的控制。

使用带命名空间的键。 按功能领域组织你的 .resw 文件("Settings""Checkout""Common"),这样随着应用增长,翻译仍然可控。

尽早用伪本地化测试。 在 debug 构建里打开它。它没有运行时开销,却能抓到那些只在德语或阿拉伯语下才会出现的布局问题。

所有用户可见的数字与日期都要格式化。 永远不要直接调用 .ToString()FormatNumberFormatDate 会为每种区域处理千位分隔符、小数符号与日期顺序。

对布局敏感的逻辑要检查 intl.IsRtl 如果你有方向性图标(箭头、尖括号)或绝对定位,请在区域为 RTL 时翻转它们。

下一步

  • 无障碍 —— 上一个主题:为控件加标签、设置地标,并支持屏幕阅读器
  • 动画 —— 下一个主题:为你的 UI 添加过渡与布局动画
  • 上下文 —— 理解 LocaleProvider 底层使用的提供程序模式
  • 表单与输入 —— 本地化表单标签、占位符与校验消息