Files

8.8 KiB
Raw Permalink Blame History

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 契约。