Files
AtomUI/docs/Core/LanguageSystem/DeveloperGuide.md
T
chinboy 3018239197 docs(i18n): add language system architecture and developer guide documentation
- Add Overview.md: design goals, system architecture diagram, core concepts, source file inventory
- Add Architecture.md: core types, Source Generator pipeline, end-to-end Dialog example, resource lifecycle, runtime language switching
- Add DeveloperGuide.md: step-by-step tutorial with QRCode example, Common shared resources, Dialog/Tour/Pagination examples, application-level i18n, runtime switching API, FAQ
- Update .gitignore: add .referenceprojects/ ignore rule
- Bump version to 5.2.0-build.3
2026-04-10 14:56:38 +08:00

18 KiB
Raw Blame History

AtomUI 多语言系统 — 开发者使用指南

本文档面向使用 AtomUI 框架开发自定义控件或应用的开发者,详细说明如何为控件和应用添加多语言支持。


1. 快速上手

为一个控件添加多语言支持只需 5 步:

  1. 配置程序集元信息(AssemblyInfo.cs)
  2. 创建语言 Provider 类(每种语言一个文件)
  3. 编译 — Source Generator 自动生成枚举和标记扩展
  4. 在 ThemeManagerBuilderExtensions 中注册语言 Provider
  5. 在 AXAML 主题中使用生成的标记扩展绑定文本

下面以一个完整的 QRCode 控件为例,逐步演示。


2. 完整示例:QRCode 控件多语言

2.1 目录结构

AtomUI.Desktop.Controls/
├── Properties/
│   └── AssemblyInfo.cs              ← 步骤 1:程序集级配置
├── QRCode/
│   ├── QRCode.cs                    ← 控件类
│   ├── QRCodeToken.cs               ← Token 定义(含 const string ID)
│   ├── Localization/                ← 步骤 2:语言 Provider
│   │   ├── zh_CN.cs
│   │   └── en_US.cs
│   └── Themes/
│       └── QRCodeTheme.axaml        ← 步骤 5:使用语言资源
├── GeneratedFiles/                  ← 步骤 3:自动生成(不要手动编辑!)
│   └── AtomUI.Generator/
│       └── AtomUI.Generator.LanguageGenerator/
│           ├── LanguageResourceConst.g.cs    ← 生成的枚举 + 标记扩展
│           └── LanguageProviderPool.g.cs     ← 生成的 Provider 注册池
└── ThemeManagerBuilderExtensions.cs  ← 步骤 4:注册

2.2 步骤 1:配置程序集元信息

在项目的 Properties/AssemblyInfo.cs 中添加两项配置:

using AtomUI.Theme.Language;
using Avalonia.Metadata;

// 1. 注册生成代码的 XAML 命名空间映射
[assembly: XmlnsDefinition("https://atomui.net", "AtomUI.Desktop.Controls.Localization")]

// 2. 指定 Source Generator 输出命名空间
[assembly: LanguageSgMetaInfo("AtomUI.Desktop.Controls.Localization")]

说明:

  • XmlnsDefinition 确保生成的标记扩展可以在 AXAML 中通过 atom: 前缀访问
  • LanguageSgMetaInfo 告诉 Source Generator 将枚举和标记扩展生成到指定命名空间
  • 这两项配置每个项目只需设置一次

2.3 步骤 2:创建语言 Provider 类

在控件目录下创建 Localization/ 文件夹,每种语言一个文件:

QRCode/Localization/zh_CN.cs

using AtomUI.Desktop.Controls.Localization;
using AtomUI.Theme.Language;

namespace AtomUI.Desktop.Controls.QRCodeLang;

[LanguageProvider(LanguageCode.zh_CN, QRCodeToken.ID)]
internal class zh_CN : LanguageProvider
{
    public const string Refresh = "点击刷新";
    public const string Expired = "二维码过期";
    public const string Scanned = "已扫描";
    
    protected override Type GetResourceKindType() => typeof(QRCodeLangResourceKind);
}

QRCode/Localization/en_US.cs

using AtomUI.Desktop.Controls.Localization;
using AtomUI.Theme.Language;

namespace AtomUI.Desktop.Controls.QRCodeLang;

[LanguageProvider(LanguageCode.en_US, QRCodeToken.ID)]
internal class en_US : LanguageProvider
{
    public const string Refresh = "Refresh";
    public const string Expired = "QR code expired";
    public const string Scanned = "Scanned";
    
    protected override Type GetResourceKindType() => typeof(QRCodeLangResourceKind);
}

关键规则:

规则 说明
特性标注 必须添加 [LanguageProvider(LanguageCode.xx, LangId)]
继承 必须继承 LanguageProvider
访问修饰符 类建议 internal,字段必须 public const string
LangId 通常使用控件 Token 的 ID(如 QRCodeToken.ID = "QRCode")
命名空间 使用独立命名空间:{项目}.{控件名}Lang(如 AtomUI.Desktop.Controls.QRCodeLang)
字段名一致 同一 LangId 下所有语言的字段名必须完全一致(大小写敏感)
GetResourceKindType() 返回生成的 {LangId}LangResourceKind 枚举类型

2.4 步骤 3:编译 — Source Generator 自动生成

编译后,Source Generator 自动生成两个文件:

生成的 LanguageResourceConst.g.cs

namespace AtomUI.Desktop.Controls.Localization
{
    // 资源键枚举 — 字段名自动从所有语言 Provider 中提取并排序
    public enum QRCodeLangResourceKind
    {
        Expired,
        Refresh,
        Scanned
    }

    // XAML 标记扩展 — 在 AXAML 中绑定语言文本
    public class QRCodeLangResourceExtension : LanguageResourceExtension<QRCodeLangResourceKind>
    {
        public QRCodeLangResourceExtension() { }
        public QRCodeLangResourceExtension(QRCodeLangResourceKind kind) : base(kind) { }
    }
}

生成的 LanguageProviderPool.g.cs

namespace AtomUI.Theme.Language
{
    internal class LanguageProviderPool
    {
        internal static IList<LanguageProvider> GetLanguageProviders()
        {
            List<LanguageProvider> languageProviders = new List<LanguageProvider>();
            languageProviders.Add(new AtomUI.Desktop.Controls.QRCodeLang.en_US());
            languageProviders.Add(new AtomUI.Desktop.Controls.QRCodeLang.zh_CN());
            // ... 同项目中其他控件的 Provider
            return languageProviders;
        }
    }
}

⚠️ 不要手动编辑 GeneratedFiles/ 目录下的文件! 它们在每次编译时会被自动重新生成。

2.5 步骤 4:注册语言 Provider

在项目的 ThemeManagerBuilderExtensions.cs 中注册:

public static IThemeManagerBuilder UseDesktopControls(this IThemeManagerBuilder themeManagerBuilder)
{
    // ... 注册 Token 和控件主题 ...

    // 注册语言 Provider(Source Generator 生成的 LanguageProviderPool)
    var languageProviders = LanguageProviderPool.GetLanguageProviders();
    foreach (var languageProvider in languageProviders)
    {
        themeManagerBuilder.AddLanguageProviders(languageProvider);
    }

    return themeManagerBuilder;
}

2.6 步骤 5:在 AXAML 主题中使用

在控件的 AXAML 主题文件中,使用生成的标记扩展绑定语言文本:

<ResourceDictionary
    xmlns="https://github.com/avaloniaui"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:atom="https://atomui.net">

    <ControlTheme x:Key="{x:Type atom:QRCode}" TargetType="atom:QRCode">
        <!-- 使用语言资源标记扩展 -->
        <Setter Property="ExpiredDescription" Value="{atom:QRCodeLangResource Expired}" />
        <Setter Property="RefreshText" Value="{atom:QRCodeLangResource Refresh}" />
    </ControlTheme>
</ResourceDictionary>

标记扩展语法:

{atom:{LangId}LangResource {FieldName}}

例如:

  • {atom:QRCodeLangResource Expired} — QRCode 的"过期"文本
  • {atom:TourLangResource Next} — Tour 的"下一步"文本
  • {atom:CommonLangResource Ok} — 通用的"确定"文本

3. 使用 Common 通用语言资源

对于跨控件共享的通用文本(如"确定"、"取消"、"提交"),AtomUI 提供了 CommonLangId.Common 分组。

3.1 Common 资源定义

定义在 AtomUI.Controls/Localization/ 中:

// AtomUI.Controls/Localization/zh_CN.cs
[LanguageProvider(LanguageCode.zh_CN, CommonLangId.Common)]
internal class zh_CN : LanguageProvider
{
    public const string Ok = "确定";
    public const string Submit = "提交";
    public const string Cancel = "取消";
    public const string Reset = "重置";
    public const string Edit = "编辑";
    public const string Delete = "删除";
    public const string Save = "保存";
    public const string NoData = "暂无数据";
    public const string Loading = "正在加载数据";
    public const string Optional = "(可选)";

    protected override Type GetResourceKindType() => typeof(CommonLangResourceKind);
}

3.2 在 AXAML 中使用 Common 资源

<!-- PopupConfirm 使用通用的 Ok/Cancel -->
<Setter Property="OkText" Value="{atom:CommonLangResource Ok}"/>
<Setter Property="CancelText" Value="{atom:CommonLangResource Cancel}"/>

<!-- Form SubmitButton 使用通用的 Submit -->
<Setter Property="Content" Value="{atom:CommonLangResource Submit}"/>

<!-- Form ResetButton 使用通用的 Reset -->
<Setter Property="Content" Value="{atom:CommonLangResource Reset}"/>

3.3 何时使用 Common vs 控件专属

场景 使用
通用操作按钮文本(确定、取消、提交等) CommonLangResource
控件特有的专业术语 控件专属 {LangId}LangResource
同一文本在不同控件中含义相同 CommonLangResource
同一文本在不同控件中含义不同 各自定义专属资源

例如,Dialog 控件虽然也有 "确定" 和 "取消",但它定义了自己的完整按钮文本集(包括 "打开"、"保存"、"丢弃" 等 Dialog 特有的选项),因此使用独立的 DialogLangResource。


4. 更多控件示例

4.1 Dialog 控件(大量语言项)

// Dialog/Localization/zh_CN.cs
namespace AtomUI.Desktop.Controls.DialogLang;

[LanguageProvider(LanguageCode.zh_CN, DialogToken.ID)]
internal class zh_CN : LanguageProvider
{
    public const string Ok = "确定";
    public const string Open = "打开";
    public const string Save = "保存";
    public const string Cancel = "取消";
    public const string Close = "关闭";
    public const string Discard = "丢弃";
    public const string Apply = "应用";
    public const string Reset = "重置";
    public const string Reload = "重新加载";
    public const string RestoreDefaults = "恢复默认值";
    public const string Help = "帮助";
    public const string SaveAll = "全部保存";
    public const string Yes = "是";
    public const string YesToAll = "全部是";
    public const string No = "否";
    public const string NoToAll = "全部否";
    public const string Abort = "中止";
    public const string Retry = "重试";
    public const string Ignore = "忽略";
    
    protected override Type GetResourceKindType() => typeof(DialogLangResourceKind);
}

4.2 Tour 控件(在 AXAML Template 内使用)

// Tour/Localization/zh_CN.cs
namespace AtomUI.Desktop.Controls.TourLang;

[LanguageProvider(LanguageCode.zh_CN, TourToken.ID)]
internal class zh_CN : LanguageProvider
{
    public const string Previous = "上一步";
    public const string Next = "下一步";
    public const string Finish = "结束导览";
    
    protected override Type GetResourceKindType() => typeof(TourLangResourceKind);
}

AXAML 中在 ControlTemplate 内部使用:

<ControlTemplate>
    <StackPanel Orientation="Horizontal">
        <atom:Button Content="{atom:TourLangResource Previous}"/>
        <atom:Button Content="{atom:TourLangResource Next}"/>
        <atom:Button Content="{atom:TourLangResource Finish}"/>
    </StackPanel>
</ControlTemplate>

4.3 Pagination 控件(含格式化字符串)

// Pagination/Localization/zh_CN.cs
namespace AtomUI.Desktop.Controls.PaginationLang;

[LanguageProvider(LanguageCode.zh_CN, PaginationToken.ID)]
internal class zh_CN : LanguageProvider
{
    public const string JumpToText = "跳至";
    public const string PageText = "页";
    public const string TotalInfoFormat = "共 ${Total} 项";
    
    protected override Type GetResourceKindType() => typeof(PaginationLangResourceKind);
}

注意:TotalInfoFormat 中的 ${Total} 是一个占位符模板,需要控件代码在运行时进行字符串替换。多语言系统只负责提供翻译后的模板字符串。


5. 为应用级页面添加多语言

多语言系统不仅适用于控件库,也适用于应用程序自身的 UI 文本。以下是 AtomUI Gallery 应用的实际示例:

5.1 配置程序集元信息

// AtomUIGallery/Properties/AssemblyInfo.cs
[assembly: LanguageSgMetaInfo("AtomUIGallery.Localization")]

5.2 定义语言 Provider

// AtomUIGallery/Workspace/Localization/WorkspaceWindowLang/zh_CN.cs
using AtomUI.Theme.Language;
using AtomUIGallery.Localization;
using AtomUIGallery.Workspace.Views;

namespace AtomUIGallery.Workspace.Localization.WorkspaceWindowLang;

[LanguageProvider(LanguageCode.zh_CN, WorkspaceWindow.LanguageId)]
internal class zh_CN : LanguageProvider
{
    public const string MenuItemSettings = "设置";
    public const string MenuItemTheme = "主题";
    public const string MenuItemLanguage = "语言";
    public const string MenuItemDarkMode = "暗黑模式";
    public const string MenuItemCompactMode = "紧凑模式";
    public const string MenuItemEnableMotion = "开启动效";
    
    protected override Type GetResourceKindType() => typeof(WorkspaceWindowLangResourceKind);
}
// 对应的 en_US.cs
[LanguageProvider(LanguageCode.en_US, WorkspaceWindow.LanguageId)]
internal class en_US : LanguageProvider
{
    public const string MenuItemSettings = "Settings";
    public const string MenuItemTheme = "Theme";
    public const string MenuItemLanguage = "Language";
    public const string MenuItemDarkMode = "Dark Mode";
    public const string MenuItemCompactMode = "Compact Mode";
    public const string MenuItemEnableMotion = "Enable Motion";
    
    protected override Type GetResourceKindType() => typeof(WorkspaceWindowLangResourceKind);
}

5.3 定义 LangId 常量

应用级页面没有 Token ID,因此自定义一个常量:

// WorkspaceWindow.cs
public partial class WorkspaceWindow
{
    public const string LanguageId = "WorkspaceWindow";
}

5.4 注册到 ThemeManagerBuilder

// AtomUIGallery/ThemeManagerBuilderExtensions.cs
public static IThemeManagerBuilder UseGalleryControls(this IThemeManagerBuilder themeManagerBuilder)
{
    themeManagerBuilder.AddControlThemesProvider(new GalleryControlThemesProvider());
    
    var languageProviders = LanguageProviderPool.GetLanguageProviders();
    foreach (var languageProvider in languageProviders)
    {
        themeManagerBuilder.AddLanguageProviders(languageProvider);
    }
    return themeManagerBuilder;
}

6. 运行时语言切换

6.1 设置默认语言

在应用初始化时设置默认语言:

// GalleryApplication.cs
public override void Initialize()
{
    base.Initialize();
    this.UseAtomUI(builder =>
    {
        builder.WithDefaultLanguageVariant(LanguageVariant.zh_CN);
        builder.UseDesktopControls();
    });
}

6.2 运行时切换

// 切换到英文
Dispatcher.UIThread.Post(() =>
{
    Application.Current!.SetLanguageVariant(LanguageVariant.en_US);
});

// 切换到中文
Dispatcher.UIThread.Post(() =>
{
    Application.Current!.SetLanguageVariant(LanguageVariant.zh_CN);
});

建议:使用 Dispatcher.UIThread.Post() 确保在 UI 线程执行。

6.3 使用 CultureInfo 切换

// 通过 CultureInfo 设置语言
var cultureInfo = CultureInfo.GetCultureInfo("zh-CN");
var variant = LanguageVariant.FromCultureInfo(cultureInfo);
Application.Current!.SetLanguageVariant(variant);

6.4 监听语言变化

var themeManager = Application.Current!.GetThemeManager();
themeManager!.LanguageVariantChanged += (sender, args) =>
{
    Console.WriteLine($"语言从 {args.OldLanguage} 切换到 {args.NewLanguage}");
};

7. 常见问题

7.1 编译时报错 QRCodeLangResourceKind 找不到

原因:Source Generator 尚未运行,或 [LanguageProvider] 特性标注不正确。

解决:

  1. 确认项目引用了 AtomUI.Generator(通常通过 AtomUI.Core 传递引用)
  2. 确认 [LanguageProvider] 特性的参数正确
  3. 重新编译项目

7.2 不同语言的字段名不一致

现象:运行时报 Language item: Xxx does not exist in ... 异常。

原因:LanguageProvider 基类的 BuildResourceDictionary 方法遍历枚举值时,会查找同名的 const string 字段。如果某种语言缺少字段,会抛出异常。

解决:确保同一 LangId 下所有语言 Provider 的字段名完全一致(大小写敏感)。

7.3 AXAML 中标记扩展不可用

原因:XmlnsDefinition 未注册生成代码的命名空间。

解决:在 AssemblyInfo.cs 中添加:

[assembly: XmlnsDefinition("https://atomui.net", "YourProject.Localization")]

7.4 语言切换后文本没有更新

原因:使用了 StaticResource 而非 DynamicResource(或生成的标记扩展)。

解决:始终使用生成的 {atom:XxxLangResource Key} 标记扩展。它继承自 DynamicResourceExtension,能自动响应语言切换。

7.5 在代码中获取语言文本

如果需要在 C# 代码中(而非 AXAML 中)获取语言文本,可以通过 Avalonia 资源系统查找:

if (Application.Current!.TryFindResource(QRCodeLangResourceKind.Expired, out var text))
{
    var expiredText = text as string; // "二维码过期" 或 "QR code expired"
}

8. 新控件多语言支持清单

为新控件添加多语言支持时,按以下清单逐项检查:

  • AssemblyInfo.cs — 确认已配置 [assembly: LanguageSgMetaInfo("...")] 和对应的 XmlnsDefinition(每个项目只需一次)
  • 创建 Localization/ 目录 — 在控件目录下创建
  • 创建 zh_CN.cs — 中文语言 Provider
  • 创建 en_US.cs — 英文语言 Provider
  • 字段名一致 — 两个文件的 public const string 字段名完全相同
  • 正确的 LangId — 使用 {ControlName}Token.ID 或自定义常量
  • 正确的命名空间 — 使用 {项目}.{控件名}Lang 格式
  • GetResourceKindType() — 返回 typeof({LangId}LangResourceKind)
  • 编译通过 — 确认 Source Generator 成功生成枚举和标记扩展
  • 注册到 Builder — 通过 LanguageProviderPool.GetLanguageProviders() 自动注册(如果是现有项目的新控件,通常无需额外操作)
  • AXAML 绑定 — 所有用户可见文本使用 {atom:{LangId}LangResource Key} 绑定
  • 验证语言切换 — 在 Gallery 应用中测试中英文切换是否生效