8.8 KiB
AtomUI 多语言公共 API
本文定义应用、类库、ViewModel、服务和 XAML 使用 AtomUI 本地化基础设施时可依赖的公共契约。 Catalog 和 XLIFF 的源文件规则见 catalog-and-xliff.md,运行时语义见 本地化运行时架构。
根 Builder
Application.UseAtomUI(...) 的配置参数由主题专用的 IThemeManagerBuilder 升级为根级 IAtomUIBuilder。
根 Builder 组合主题、本地化、控件包和运行时初始化,不把本地化重新塞进主题 Builder。
IAtomUIBuilder
├── IThemeBuilder
├── ILocalizationBuilder
└── Application initializers
常用能力继续通过根 Builder 扩展方法暴露,避免调用方依赖内部子 Builder:
this.UseAtomUI(builder =>
{
builder.UseLanguages(
defaultLanguage: LanguageTags.ZhCN,
supportedLanguages:
[
LanguageTags.EnUS,
LanguageTags.ZhCN,
LanguageTags.ZhTW
]);
builder.UseDesktopControls();
});
命名必须保持以下层次:
UseAtomUI()是进入 AtomUI 框架的根入口。- Builder 内使用
UseLanguages()、UseDesktopControls()等领域名称,不重复AtomUI前缀。 defaultLanguage表示启动后首次提交的应用语言,不改变每个 Catalog 固定的en-US源语言。defaultLanguage必须属于supportedLanguages;Builder 对输入集合做防御性复制,并按首次出现顺序去重。- 未调用
UseLanguages()时采用en-US作为默认语言和唯一支持语言,保证最小应用可启动。
UseLanguages() 不接收包名、程序集、Descriptor 或语言包列表。应用 Catalog、组件 Catalog 和静态语言包
通过生成链注册,Builder 配置顺序不得影响最终结果。
LanguageTag
语言身份使用开放且不可变的 BCP 47 值类型:
public readonly record struct LanguageTag
{
public string Value { get; }
public static LanguageTag Parse(string value);
public static bool TryParse(string? value, out LanguageTag language);
public static LanguageTag FromCultureInfo(CultureInfo culture);
}
LanguageTag 构造后必须处于规范形式:language subtag 小写、script 首字母大写、region 大写,其他 subtag 按
BCP 47 规范化;已知 grandfathered/alias 映射使用仓库固定的 IANA/CLDR 数据。相等性和哈希只比较规范化后的
完整标签,使用 ordinal 语义。ToString() 返回规范标签,default(LanguageTag) 是无效值,任何公共入口都必须
拒绝它,不能把空值解释为系统语言或 en-US。
Parse 和 TryParse 用于配置、命令行、持久化和网络数据等字符串边界。普通程序代码应使用生成式常量:
LanguageTags.EnUS
LanguageTags.EnGB
LanguageTags.ZhCN
LanguageTags.ZhHans
LanguageTags.ZhTW
LanguageTags.ZhHant
LanguageTags.JaJP
LanguageTags.ArSA
LanguageTags 根据仓库固定版本的 CLDR/IANA 数据生成常用语言、脚本和区域标签。BCP 47 可以组合脚本、
区域、variant、extension 和 private-use subtag,因此不得用 enum 声称穷举所有合法值。第三方私有标签应在
自己的静态类中封装:
public static class AcmeLanguageTags
{
public static LanguageTag EnAcme { get; } = LanguageTag.Parse("en-x-acme");
}
LanguageDefinition
LanguageTag 只表达身份。语言选择器显示名称、格式化 Culture 和文字方向属于注册元数据:
public sealed record LanguageDefinition(
LanguageTag Tag,
CultureInfo FormattingCulture,
string NativeName,
LanguageTextDirection TextDirection);
标准标签优先使用生成的 CLDR/.NET 元数据。无法映射到 .NET CultureInfo 的私有标签必须由应用显式提供
LanguageDefinition;系统不得静默改用 InvariantCulture。
var privateLanguage = LanguageTag.Parse("en-x-acme");
builder.Localization.AddLanguageDefinition(new LanguageDefinition(
privateLanguage,
CultureInfo.GetCultureInfo("en-US"),
"Acme English",
LanguageTextDirection.LeftToRight));
builder.UseLanguages(privateLanguage, [privateLanguage]);
同一标签重复注册显式 LanguageDefinition 会在启动构建时失败;这与 supportedLanguages 中允许重复并去重的
集合语义不同。
ILanguageManager
ILanguageManager 是全局应用语言状态的唯一所有者:
public interface ILanguageManager
{
LanguageState Current { get; }
IReadOnlyList<LanguageDefinition> SupportedLanguages { get; }
event EventHandler<LanguageChangedEventArgs>? LanguageChanged;
LanguageChangeResult ChangeLanguage(LanguageTag language);
}
静态语言包已经在应用构建时编译完成,因此全局切换是同步 Snapshot 提交,不设计伪异步 API。
ChangeLanguage() 必须在 UI 线程调用;切换到当前语言返回 NoOp,不重复发布资源通知或事件。
LanguageChangeResult 只有 Committed 和 NoOp 两种成功状态,并携带旧/新 LanguageState。未支持标签抛出
LanguageNotSupportedException,错误线程或已释放 Manager 抛出 InvalidOperationException;静态 Snapshot 不存在
表示启动构建不变量被破坏,不能包装成普通 Failed 结果继续运行。
ILanguageManager 和 ILocalizer 保持 DI 中立。AtomUI 将实例注册到自身运行时服务位置供控件使用,应用可以把
同一实例注册进 Microsoft.Extensions.DependencyInjection、Splat 或自己的容器,不创建第二个 Manager。
ILocalizer
ViewModel、服务和普通 C# 代码通过 ILocalizer 读取当前 Snapshot:
public interface ILocalizer
{
string Get<TResourceKind>(TResourceKind key)
where TResourceKind : struct, Enum;
string Format<TResourceKind>(TResourceKind key, params object?[] arguments)
where TResourceKind : struct, Enum;
}
var title = localizer.Get(LoginLangResourceKind.Title);
var message = localizer.Format(OrderLangResourceKind.ItemCount, count);
Get 和 Format 根据 enum CLR 类型定位 Catalog,通过 Generator 输出的 enum member switch 定位资源 slot。enum
成员名是构建期 Catalog Key,但运行时不得调用 ToString() 或 Enum.GetName() 构造资源路径。格式化使用
LanguageState.FormattingCulture,参数签名在构建期跨语言校验。
XAML 动态资源自动响应语言变化。ViewModel 如果要缓存已经本地化的字符串,必须订阅 LanguageChanged 并重新
计算;更推荐保存稳定资源 enum 或业务状态,在属性 getter/投影阶段调用 ILocalizer。
LanguageCatalogUnitDescriptor
生成的 Catalog registration 使用字符串 Key 描述资源项,公共 descriptor 不再同时暴露数字 Id 和字符串
Name:
public sealed record LanguageCatalogUnitDescriptor
{
public LanguageCatalogUnitDescriptor(string key, bool isFormatted = false);
public string Key { get; }
public bool IsFormatted { get; }
}
Key 必须与 Catalog enum 成员名及 XLIFF unit id 完全一致,使用 ordinal、区分大小写的比较语义。构造函数拒绝
空值和空白,Catalog descriptor 拒绝重复 Key;Key 与 enum symbol 的一致性由 Generator 和 Build Tasks 校验。
LanguageCatalogUnitDescriptor.Id、Name 及接受数字 ID 的构造函数不保留兼容层,避免重新形成两套身份来源。
XAML namespace 与 Markup Extension
应用为 Catalog 所在 C# namespace 声明普通 AXAML 别名:
<UserControl
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:login="using:MyApplication.Features.Login.Localization"
xmlns:atom="using:AtomUI.Controls">
<TextBlock Text="{login:LoginLangResource Title}" />
<TextBlock Text="{atom:DatePickerLangResource Today}" />
</UserControl>
login 只是当前 AXAML 文件定义的 XML namespace 别名,不是 AtomUI 固定关键字。应优先使用 login、
orders、settings 等功能域名称,而不是含义模糊的全局 i18n 别名。
Generator 为 LoginLangResourceKind 生成 LoginLangResourceExtension。Avalonia 允许在 Markup Extension 使用处
省略 Extension 后缀,因此 {login:LoginLangResource Title} 在 XAML 编译期解析为
LoginLangResourceKind.Title,不是运行时字符串查找。
公共 API 中禁止的形态
builder.UseAtomUILanguages(...);
builder.UseAtomUIZhHans();
builder.UseAtomUIJaJP();
builder.UseLanguages(packages: ...);
localizer.Get("Login.Title");
<TextBlock Text="{atomui:Loc DatePicker.Today}" />
这些形态分别造成重复命名、API 数量随语言增长、语言包运行时泄漏、弱类型字符串键或破坏既有 XAML 契约。