Files
AtomUI/docs/Generator/LanguageGenerator.md

14 KiB
Raw Permalink Blame History

LanguageGenerator 设计原理

本文档详细解析 LanguageGenerator 源生成器的内部实现,它是 AtomUI 多语言系统能够在 AXAML 中被类型安全引用的底层基础设施。


1. 解决的核心问题

AtomUI 的多语言系统需要解决:

  • 语言文本定义在 C# 中:每个控件的每种语言创建一个 LanguageProvider 子类,用 public const string 字段定义翻译文本。
  • 语言文本消费在 AXAML 中:Theme 文件通过 {atom:UploadLangResource Uploading} 引用本地化文本。
  • 运行时切换:用户切换语言时,需要动态替换 ResourceDictionary 中的资源值。

LanguageGenerator 的职责是 自动从 LanguageProvider 类中提取所有字段名,生成枚举(资源键)、XAML 标记扩展类,以及 Provider 注册池。


2. 输入与输出

2.1 输入:标记注解

[LanguageProvider(LanguageCode, LangId)] — 标记语言提供者类

// src/AtomUI.Desktop.Controls/Upload/Localization/en_US.cs
namespace AtomUI.Desktop.Controls.UploadLang;

[LanguageProvider(LanguageCode.en_US, UploadToken.ID)]
internal class en_US : LanguageProvider
{
    public const string Uploading = "Uploading...";
    public const string Pending = "Pending...";
    public const string DragUploadHead = "Click or drag file to this area to upload";
    
    protected override Type GetResourceKindType() => typeof(UploadLangResourceKind);
}
// src/AtomUI.Desktop.Controls/Upload/Localization/zh_CN.cs
namespace AtomUI.Desktop.Controls.UploadLang;

[LanguageProvider(LanguageCode.zh_CN, UploadToken.ID)]
internal class zh_CN : LanguageProvider
{
    public const string Uploading = "上传中...";
    public const string Pending = "等待调度...";
    public const string DragUploadHead = "点击或拖动文件到此区域进行上传";
    
    protected override Type GetResourceKindType() => typeof(UploadLangResourceKind);
}

[LanguageSgMetaInfo] — 程序集级注解,指定生成代码的目标命名空间

// src/AtomUI.Desktop.Controls/Properties/AssemblyInfo.cs
[assembly: LanguageSgMetaInfo("AtomUI.Desktop.Controls.Localization")]

2.2 输出产物

产物 1:LanguageResourceConst.g.cs

为每个 LanguageId(即每个控件的语言组)生成一对枚举 + 标记扩展类:

namespace AtomUI.Desktop.Controls.Localization
{
    // 枚举:合并该 LanguageId 下所有语言 Provider 的字段名(去重)
    public enum UploadLangResourceKind
    {
        DragUploadHead,
        Pending,
        Uploading
    }

    // XAML 标记扩展:在 AXAML 中作为 {atom:UploadLangResource Uploading} 使用
    public class UploadLangResourceExtension : LanguageResourceExtension<UploadLangResourceKind>
    {
        public UploadLangResourceExtension() { }
        public UploadLangResourceExtension(UploadLangResourceKind kind) : base(kind) { }
    }
}

产物 2:LanguageProviderPool.g.cs

一个静态方法,实例化当前程序集中所有 [LanguageProvider] 类:

namespace AtomUI.Theme.Language
{
    internal class LanguageProviderPool
    {
        internal static IList<LanguageProvider> GetLanguageProviders()
        {
            List<LanguageProvider> languageProviders = new List<LanguageProvider>();
            languageProviders.Add(new AtomUI.Desktop.Controls.UploadLang.en_US());
            languageProviders.Add(new AtomUI.Desktop.Controls.UploadLang.zh_CN());
            // ... 所有语言 Provider 实例
            return languageProviders;
        }
    }
}

3. 内部管线详解

3.1 管线概览

LanguageGenerator.Initialize()
│
├─ languageProvider ─────────────────────────────────────────────┐
│  ForAttributeWithMetadataName("[LanguageProvider]")            │
│  → LanguageProviderWalker 遍历                                 │
│  → 收集 LanguageCode / LanguageId / 字段名+值 / 命名空间       │
│  → Collect                                                     │
│                                                                │
└─ RegisterImplementationSourceOutput ───────────────────────────┘
   │
   ├─ LangResourceKeyClassSourceWriter.Write()
   │  → 生成 LanguageResourceConst.g.cs
   │     ├─ 按 LanguageId 分组
   │     ├─ 合并同组所有 Provider 的字段名(去重 + 排序)
   │     ├─ XxxLangResourceKind 枚举
   │     └─ XxxLangResourceExtension 类
   │
   └─ LanguageProviderPoolClassSourceWriter.Write()
      → 生成 LanguageProviderPool.g.cs
         └─ GetLanguageProviders() 方法

3.2 LanguageProviderWalker(语言提供者遍历器)

文件:src/AtomUI.Generator/Language/LanguageProviderWalker.cs

继承 CSharpSyntaxWalker,执行三项任务:

① 提取程序集元信息

在构造函数中调用 ExtractLanguageMetaInfo(),从程序集的 [LanguageSgMetaInfo] 注解中提取 TargetNamespace。这决定了生成代码的目标命名空间(如 AtomUI.Desktop.Controls.Localization)。

② 遍历类声明

VisitClassDeclaration 提取:

  • ClassName:类名(如 en_US)
  • Namespace:所在命名空间(如 AtomUI.Desktop.Controls.UploadLang)
  • LanguageCode:从 [LanguageProvider] 注解的第一个参数获取(如 "en_US")
  • LanguageId:从注解的第二个参数获取(如 "Upload",即 UploadToken.ID 的编译时常量值)

③ 遍历字段声明

VisitFieldDeclaration 收集每个 public const string 字段的名称和初始值:

  • 字段名作为资源键(如 Uploading)
  • 字段值作为翻译文本(如 "Uploading...")

3.3 LanguageId 分组机制

生成器使用 LanguageId(即 Token 的 ID 常量,如 "Upload"、"Dialog")将多个语言 Provider 分组。同一 LanguageId 下的所有 Provider(如 en_US 和 zh_CN)共享同一个生成的枚举类型。

LanguageId = "Upload"
├── UploadLang.en_US  → { Uploading, Pending, DragUploadHead }
└── UploadLang.zh_CN  → { Uploading, Pending, DragUploadHead }
    ↓ 合并去重
    UploadLangResourceKind { DragUploadHead, Pending, Uploading }

这确保了不同语言的 Provider 定义相同的字段集合时,生成的枚举是一致的。

3.4 LangResourceKeyClassSourceWriter(资源键写入器)

文件:src/AtomUI.Generator/Language/LangResourceKeyClassSourceWriter.cs

核心逻辑:

  1. 按 LanguageId 分组:将所有 LanguageInfo 按 LanguageId 分组到 Dictionary<string, List<LanguageInfo>>。
  2. 合并字段名:对同一组内所有 Provider 的字段名取并集(UnionWith),然后按字母排序。
  3. 生成枚举:枚举名为 {LanguageId}LangResourceKind,成员为排序后的字段名。
  4. 生成标记扩展类:类名为 {LanguageId}LangResourceExtension,继承 LanguageResourceExtension<{LanguageId}LangResourceKind>。
  5. 目标命名空间:优先使用 [LanguageSgMetaInfo] 指定的 TargetNamespace;若未指定,使用第一个 Provider 的命名空间。

3.5 LanguageProviderPoolClassSourceWriter(Provider 池写入器)

文件:src/AtomUI.Generator/Language/LanguageProviderPoolClassSourceWriter.cs

生成 LanguageProviderPool 类,内含 GetLanguageProviders() 方法。该方法使用完全限定名(Namespace.ClassName)实例化每个 Provider,并按命名空间 + 类名排序以保证生成结果稳定。


4. 语言资源从定义到消费的完整链路

以 Upload 控件的 Uploading 文本为例:

4.1 定义阶段

// 开发者编写 en_US.cs
[LanguageProvider(LanguageCode.en_US, UploadToken.ID)]
internal class en_US : LanguageProvider
{
    public const string Uploading = "Uploading...";
    protected override Type GetResourceKindType() => typeof(UploadLangResourceKind);
}

// 开发者编写 zh_CN.cs
[LanguageProvider(LanguageCode.zh_CN, UploadToken.ID)]
internal class zh_CN : LanguageProvider
{
    public const string Uploading = "上传中...";
    protected override Type GetResourceKindType() => typeof(UploadLangResourceKind);
}

4.2 生成阶段(编译时)

// 自动生成 — LanguageResourceConst.g.cs
namespace AtomUI.Desktop.Controls.Localization
{
    public enum UploadLangResourceKind { DragUploadHead, Pending, Uploading }
    
    public class UploadLangResourceExtension : LanguageResourceExtension<UploadLangResourceKind>
    {
        public UploadLangResourceExtension() { }
        public UploadLangResourceExtension(UploadLangResourceKind kind) : base(kind) { }
    }
}

// 自动生成 — LanguageProviderPool.g.cs
languageProviders.Add(new AtomUI.Desktop.Controls.UploadLang.en_US());
languageProviders.Add(new AtomUI.Desktop.Controls.UploadLang.zh_CN());

4.3 注册阶段(运行时启动)

ThemeManager 调用 LanguageProviderPool.GetLanguageProviders() 获取所有 Provider 实例。LanguageProvider 基类通过反射读取子类的 public const string 字段,将字段名(如 "Uploading")映射到 UploadLangResourceKind.Uploading 枚举值,以此作为 ResourceKey 写入对应语言的 ResourceDictionary。

4.4 消费阶段(AXAML 渲染)

<!-- UploadDefaultDropAreaTheme.axaml -->
<Setter Property="Header" Value="{atom:UploadLangResource DragUploadHead}" />

AXAML 解析器将 {atom:UploadLangResource DragUploadHead} 解析为 UploadLangResourceExtension(UploadLangResourceKind.DragUploadHead)。由于继承自 DynamicResourceExtension,切换语言时 ResourceDictionary 被替换,绑定自动更新。

4.5 语言切换

当用户调用 ThemeManager.Current.SetLanguage(LanguageCode.zh_CN) 时:

  1. ThemeManager 替换 Application.Resources 中的语言 ResourceDictionary。
  2. 所有通过 DynamicResourceExtension 绑定的语言资源自动更新。
  3. UI 即时反映新语言文本,无需重新加载。

5. 关键数据结构

LanguageInfo

internal class LanguageInfo
{
    public string? TargetNamespace { get; set; }  // 来自 [LanguageSgMetaInfo]
    public string Namespace { get; set; }          // Provider 类的命名空间
    public string LanguageId { get; set; }         // 如 "Upload"(来自注解第二个参数)
    public string LanguageCode { get; set; }       // 如 "en_US"(来自注解第一个参数)
    public string ClassName { get; set; }          // 如 "en_US"
    public Dictionary<string, string> Items { get; set; }  // 字段名 → 文本值
}

6. 与 LanguageResourceExtension 运行时基类的关系

生成的 XxxLangResourceExtension 继承自 LanguageResourceExtension<TResourceKind>,其定义在 AtomUI.Core:

// src/AtomUI.Core/Language/LanguageResourceExtension.cs
public abstract class LanguageResourceExtension<TResourceKind> : DynamicResourceExtension
    where TResourceKind : Enum
{
    public TResourceKind? Kind
    {
        get => (TResourceKind?)ResourceKey; 
        set => ResourceKey = value; 
    }
    
    public new IBinding ProvideValue(IServiceProvider serviceProvider)
    {
        base.ProvideValue(serviceProvider);
        // 特殊处理:当目标不是 StyledElement 时(如 Application 级资源),
        // 设置 anchor 为 Application.Current,确保资源查找正确
        var provideTarget = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
        if (provideTarget?.TargetObject is not StyledElement)
        {
            this.SetAnchor(Application.Current);
        }
        return this;
    }
}

与 TokenResourceExtension 的差异:LanguageResourceExtension 重写了 ProvideValue,增加了 SetAnchor 逻辑。这是因为语言资源可能在非 StyledElement 上下文中使用(如 Application 级别的默认值),需要显式设置锚点以确保资源查找链路正确。


7. GetResourceKindType() 的闭环设计

与 Token 系统的 GetTokenKindType() 对应,每个 LanguageProvider 必须实现 GetResourceKindType():

protected override Type GetResourceKindType() => typeof(UploadLangResourceKind);

运行时 LanguageProvider 基类通过此方法获取枚举类型,将 const string 字段名与枚举成员进行匹配,构建 ResourceKey → 文本值 的映射。完整闭环:

en_US.Uploading = "Uploading..." (C# const 字段)
    ↓ 源生成器
UploadLangResourceKind.Uploading (枚举值)
    ↓ GetResourceKindType() + 反射匹配
ResourceDictionary[UploadLangResourceKind.Uploading] = "Uploading..."
    ↓ DynamicResourceExtension
{atom:UploadLangResource Uploading} (AXAML 消费)
    ↓ 语言切换
ResourceDictionary[UploadLangResourceKind.Uploading] = "上传中..."

8. 与 Token 生成器的对比

维度 TokenResourceKeyGenerator LanguageGenerator
触发注解 [GlobalDesignToken] / [ControlDesignToken] [LanguageProvider]
数据来源 C# 属性({ get; set; }) C# const string 字段
分组键 Token 类名 LanguageId(注解第二个参数)
生成枚举 SharedTokenKind / XxxTokenKind XxxLangResourceKind
生成标记扩展 XxxTokenResourceExtension XxxLangResourceExtension
注册池 ControlTokenTypePool(返回 Type 列表) LanguageProviderPool(返回实例列表)
运行时基类 TokenResourceExtension<T> LanguageResourceExtension<T>
闭环方法 GetTokenKindType() GetResourceKindType()
动态切换 主题切换时重新计算 Token 值 语言切换时替换 ResourceDictionary
目标命名空间 {控件NS}.DesignTokens [LanguageSgMetaInfo] 指定