9.0 KiB
AtomUI 多语言运行时架构
本文定义本地化运行时的状态所有权、启动构建、查询、回退和语言切换语义。运行时位于
AtomUI.Localization,不依赖 ThemeManager 的内部事务或语言字段。
源码不使用 Runtime/ 作为兜底目录,也不把 Runtime 作为内部类型的通用后缀。应用生命周期拥有的服务组合
为 LocalizationHost,当前语言的原子状态持有者为 LanguageContext,一次完整发布为
LanguageRevision。物理目录遵循概览文档定义的 Catalog/、Resources/、Services/ 加根目录核心模型的
紧凑分组。
核心组件
LanguageManager
├── LanguageCatalogRegistry
├── IReadOnlyDictionary<LanguageTag, LanguageSnapshot>
├── LanguageContext
├── LanguageResourceProvider
├── LanguageState
└── Localizer
| 组件 | 职责 | 不负责 |
|---|---|---|
LanguageCatalogRegistry |
冻结 Catalog schema、slot、翻译来源和语言元数据 | 当前语言、资源通知 |
LanguageSnapshot |
保存某一语言完成回退后的全部资源值 | 解析 XLIFF、动态修改 |
LanguageContext |
原子持有并发布当前 LanguageRevision |
构建 Snapshot、发布资源通知 |
LanguageRevision |
把同一次提交的 Snapshot 和 State 绑定为不可分割值 | 维护支持语言集合 |
LanguageManager |
验证并提交全局语言变化,发布状态和事件 | 主题事务、下载语言包 |
LanguageResourceProvider |
把当前 Snapshot 投影为 Avalonia 动态资源 | 保存第二份翻译状态 |
Localizer |
为 C# 调用提供强类型查询和格式化 | 字符串 Catalog 路径解析 |
LocalizationHost |
持有构建完成的 Manager、Localizer、Provider 和 Snapshot 集合并管理释放 | 执行查询或语言切换算法 |
启动构建
UseAtomUI() 按以下顺序构建本地化运行时:
- 创建根
IAtomUIBuilder,并接入应用生成的 Language Module bootstrap。 - 执行用户配置;
UseXxxControls()等扩展注册各自生成的 Language Module。 ILocalizationBuilder收集 Catalog descriptor、内置 Translation Bundle、语言包 Bundle 和应用 Override。- 规范化
defaultLanguage、supportedLanguages与LanguageDefinition;支持语言按首次出现顺序去重,显式LanguageDefinition仍要求标签唯一,并验证 Culture 与方向元数据。 - 构建并冻结
LanguageCatalogRegistry,解析来源优先级、Catalog 契约和 slot。 - 为每个支持语言构建完整不可变
LanguageSnapshot;任何必需资源无法解析时启动失败。 - 创建初始
LanguageRevision并交给唯一LanguageContext,再初始化LanguageManager和稳定LanguageResourceProvider。 - 将 Provider 挂载到 Application 资源链,并注册
ILanguageManager、ILocalizer的同一运行时实例。 - 由
LocalizationHost持有构建完成的本地化服务和生命周期,在完整初始资源可用后再让应用进入首帧, 不在首帧后补做默认语言切换。
Builder 只收集输入,Build() 后全部注册表冻结。运行时不允许追加 Catalog 或 Translation Bundle。
Catalog registry
Registry 为每个 Catalog 分配进程内稳定 slot,并记录:
- 生成式 Catalog ID、enum CLR 类型和稳定 Key 到 slot 的映射。
- 稳定 unit Key、enum member 到 Catalog 内部 slot 的生成式映射。
- 每个语言的 Translation Bundle、来源优先级和源文本指纹。
en-US完整源文本。- 用于 Avalonia enum resource key 查询的生成式映射。
Registry 使用 Type 定位 Catalog,并通过 Generator 输出的 enum member switch 定位 slot。enum 底层数值只参与
编译后的 switch 分派,不是可序列化的 unit identity;Registry 不得通过 GetFields()、Enum.GetNames()、
Enum.GetName()、ToString()、Attribute 反射或 Activator.CreateInstance() 发现 Catalog 或构造 Key。
LanguageSnapshot
Snapshot 是某个请求语言的完整、不可变、已经完成回退解析的资源表:
LanguageSnapshot
├── RequestedLanguage
├── Resolved values by catalog slot and unit slot
├── Resolved source language per value
└── Revision-independent immutable storage
查询热路径不再遍历 BCP 47 回退链,也不合并 ResourceDictionary。Snapshot 构建阶段完成全部解析,
ILocalizer.Get() 和 Avalonia 资源查询只执行 slot 定位与数组读取。
Snapshot 不暴露可变 Dictionary,不缓存调用方格式化结果。所有支持语言 Snapshot 在启动时构建,确保静态 语言切换为确定的内存引用交换,不在用户操作时突然解析大量资源。
LanguageState
public sealed record LanguageState(
LanguageTag CurrentLanguage,
CultureInfo FormattingCulture,
LanguageTextDirection TextDirection,
long Revision);
CurrentLanguage是当前请求并已提交的文本语言。FormattingCulture用于ILocalizer.Format()以及 AtomUI 控件的数字、日期和时间显示。TextDirection是 LTR 或 RTL,供根视觉宿主投影FlowDirection。Revision每次成功切换递增,NoOp不递增。
语言、格式化 Culture 和文字方向相关但不等价。应用可以为私有语言标签提供自定义 Culture 和方向。
回退算法
每个 Catalog、每个 unit 独立使用同一确定链:
exact tag -> script/parent candidates -> neutral language -> en-US
示例:
zh-Hant-HK -> zh-Hant -> zh -> en-US
zh-CN -> zh-Hans -> zh -> en-US
zh-TW -> zh-Hant -> zh -> en-US
fr-CA -> fr -> en-US
候选链由 BCP 47 结构和固定 CLDR likely-subtags 数据生成,不用字符串 Split('-') 临时猜测。zh-CN 不把
zh-TW 作为候选,反向同理。只有显式提供的中性 zh Translation Bundle 才能被两个脚本族共同使用。
en-US 是最终安全值,但对于非英语支持语言,构建时直接落到 en-US 不计为翻译覆盖;这一规则防止应用
声明支持日语却悄悄显示整页英文。
语言切换事务
静态语言切换使用小型、独立的同步提交,不复用 ThemeTransaction:
- 验证 UI 线程访问、Manager 未释放且目标属于支持语言;失败分别抛出文档规定的类型化异常。
- 目标与当前语言相同则返回
NoOp。 - 读取启动时构建的目标 Snapshot 和对应
LanguageDefinition。 - 创建包含目标 Snapshot 与新
LanguageState的LanguageRevision,并在提交点由LanguageContext原子发布。 LanguageResourceProvider发布一次 hosted resources changed 通知。- 根文字方向投影与 Snapshot 属于同一提交,不暴露文本已变而方向未变的状态。
- 发布一次
LanguageChanged,事件参数包含旧/新 State 和结果 revision。
提交前失败不改变旧状态;提交点之后不执行可能失败的解析、加载或用户代码。事件订阅方抛出异常不得回滚 已提交语言,也不得阻止其他订阅方按既定事件策略收到通知。
Stable LanguageResourceProvider
Provider 在 Application 初始化时只挂载一次。它始终从 LanguageManager 的当前 Snapshot 读取,不为每种语言
维护一份可变 Avalonia ResourceDictionary,切换时也不从 MergedDictionaries 删除或添加字典。
Generator 生成的 XxxLangResourceExtension 继续基于 enum key 创建动态资源引用。Provider 负责把 exact enum
类型和值映射到 Catalog/unit slot。语言通知只使已有动态资源重新查询,不替换 Markup Extension 或资源 key。
非 StyledElement 的 XAML 目标如果只能在 ProvideValue 时取得静态值,应通过 ILocalizer 读取当前 Snapshot;
这类对象不会假装获得自身不存在的 Avalonia 动态资源生命周期。
线程模型
ChangeLanguage()、Provider 挂载和资源通知只在 UI 线程发生。- Snapshot 与 State 使用不可变对象和原子引用发布,
ILocalizer.Get()可从后台线程无锁读取一个完整 revision。 - 单次
Get()或Format()必须只观察一个 Snapshot;切换不能让一次格式化混用两个语言 revision。 - 事件在 UI 线程发布。后台服务需要响应语言变化时自行调度耗时工作,不阻塞提交路径。
Culture 策略
AtomUI 默认不修改以下全局状态:
CultureInfo.CurrentCulture
CultureInfo.CurrentUICulture
CultureInfo.DefaultThreadCurrentCulture
CultureInfo.DefaultThreadCurrentUICulture
AtomUI 控件必须读取 LanguageState.FormattingCulture,不能依赖调用线程碰巧具有相同 Culture。应用确实需要
同步进程 Culture 时,应在应用层订阅已提交的 LanguageChanged 并采用显式策略;该行为不进入 Manager 默认语义。
生命周期
LanguageManager 与 Application 生命周期一致。释放时必须解绑 Application 资源宿主、根 FlowDirection 投影和
内部订阅,并清空公开事件。Snapshot 和生成表不持有 Visual、ViewModel、DI Scope 或翻译包文件句柄。