跳转到主内容

SeatFlow 基于 .NET .resx 的国际化系统架构、资源文件结构、XAML/C# 使用规范和 i18n.py 脚本工具

国际化系统

架构概述

SeatFlow 使用标准 .NET .resx 资源文件实现国际化。资源文件位于 SeatFlow.Presentation.Avalonia/Lang/ 目录,由一个中性语言文件、一个英文卫星文件和脚本维护的强类型访问器类组成。

SeatFlow.Presentation.Avalonia/Lang/
├── Resources.resx          # 中性语言(zh-CN),约 720 个键
├── Resources.en-US.resx    # 英文卫星程序集
├── Resources.Designer.cs   # 强类型访问器(由 i18n.py sync 生成/维护)
└── .backup/                # 自动备份目录(已 gitignore)

设计决策

  • .resx 格式而非 .json 或 .po,因为 .NET 原生支持且与 {x:Static} 在 Avalonia XAML 中完美集成
  • 不使用 Visual Studio 的 PublicResXFileCodeGenerator(与 dotnet build 不兼容),改用 Designer.cs 并由脚本生成
  • 使用 python3 scripts/i18n.py 脚本统一管理三文件的增删改查和同步
  • 2.1.0(M5)清理了 107 个无引用键(含 Home_/Freeform_ 系列的废弃变体);新增/修改键一律用脚本,避免三文件漂移

资源键命名规范

格式:{Category}_{MeaningfulName}(PascalCase,下划线分隔)

已知分类前缀

前缀 用途
About_ 关于页
App_ 应用级消息
Common_ 共享 UI 标签(如 Common_OK、Common_Cancel)
ConfigBlock_ 策略配置块编辑器
Data_ 数据加载/保存
DragDrop_ 文件拖放遮罩与提示
Freeform_ 自由布局编辑器(已并入「会场与布局」页)
Gender_ 性别标签
Guide_ 引导系统
Home_ 欢迎卡与工作台空态(原首页遗留键,仍被 WelcomeCardViewModel 等引用)
Lang_ 语言名称
Member_ 人员管理
Nav_ 导航栏
SeatPosition_ 座位位置选择器
SeatSets_ .seatsets 打包导入导出
Seating_ 排座工作台
Settings_ 设置页
Shell_ 外壳(导航/抽屉/紧凑布局)
Snapshot_ 历史快照
Startup_ 启动守卫
Strategy_ 策略配置
StudentPicker_ 学生选择器
Telemetry_ 遥测同意与上报
Theme_ 主题名称
Update_ 自动更新
Venue_ 会场与布局
Watchdog_ 看门狗服务
Zoom_ 缩放级别

格式字符串

资源值中使用 {0}、{1} 等占位符,键名建议以 Fmt 结尾标识。

<data name="Snapshot_VenuesLoadedFmt" xml:space="preserve">
  <value>已加载 {0} 个会场</value>
</data>

当前支持语言

语言 文件 状态
中文(简体) Resources.resx(中性语言) 完整,~720 键
英语(美国) Resources.en-US.resx 持续维护

添加新语言

  1. 在 Lang/ 目录下创建 Resources.xx-XX.resx 文件(例如 Resources.ja-JP.resx)
  2. 将所有键翻译为目标语言
  3. 运行 python3 scripts/i18n.py check 验证一致性
  4. 无需修改 C# 代码——.NET 运行时自动按 CultureInfo.CurrentUICulture 加载对应卫星程序集
  5. 在 AppSettings.Language 中添加对应语言代码

XAML 中使用

属性语法(推荐,编译绑定支持)

<TextBlock Text="{x:Static lang:Resources.Settings_Title}" />
<Button Content="{x:Static lang:Resources.Common_OK}" />

命名空间声明

xmlns:lang="using:SeatFlow.Presentation.Avalonia.Lang"

重要限制

  • 仅支持属性语法:<TextBlock Text="{x:Static ...}" />
  • 不支持元素内容语法:以下写法不会正确解析
<!-- 错误:元素内容语法不适用于 {x:Static} -->
<Button>
  <x:Static xmlns:lang="..." Member="lang:Resources.Common_OK" />
</Button>

C# 中使用

标准用法

using SeatFlow.Presentation.Avalonia.Lang;

// 直接引用
StatusMessage = Resources.Settings_Saved;

// 带格式参数
StatusMessage = string.Format(Resources.Snapshot_VenuesLoadedFmt, count);

// 条件判断
if (Resources.Culture.TwoLetterISOLanguageName == "zh")
{
    // 中文逻辑
}

Window 子类中的特殊处理

在继承自 Window 的类(DialogWindow、InputWindow)中,Resources 解析为 Window.Resources(IResourceDictionary)。必须使用完全限定名:

// 正确:在 Window 子类中使用完全限定名
string title = Lang.Resources.Settings_Title;

// 错误:会产生编译错误
string title = Resources.Settings_Title; // 解析为 Window.Resources

ViewModel 中的使用

ViewModel 继承自 ViewModelBase(继承 ObservableObject),不存在 Window.Resources 冲突,直接使用 Resources.xxx 即可。

语言切换流程

语言统一通过 App.ApplyLanguage(language) 应用:同时设置 CultureInfo.CurrentCulture / CurrentUICulture / DefaultThreadCurrent* 与 Lang.Resources.Culture,使 {x:Static} 与 Resources.xxx 一致切换。

启动时

App.Initialize()
  ├── ApplyLanguageFromSettings()     ← 必须在 XAML 加载前调用
  │    ├── 从 AppSettings 读取 Language 配置
  │    └── ApplyLanguage(language)
  └── AvaloniaXamlLoader.Load(this)   ← XAML 加载时 {x:Static} 已正确解析
  • 桌面端:ApplyLanguageFromSettings() 内部用 Task.Run(...).GetAwaiter().GetResult() 读取设置(UI 调度器尚未就绪,避免死锁);导入 .seatsets 后 App.RefreshAfterImportAsync() 会重新应用语言。
  • 浏览器端:WASM 禁止同步阻塞等待(Cannot wait on monitors on this runtime),语言由 SeatFlow.Browser/Program.Main 在 Avalonia 启动前 await 预加载(App.ApplyLanguage)。

运行中切换(重启/重新加载后生效)

设置页保存语言后弹出确认对话框:桌面点「立即重启」自动重启进程,Web 端手动重新加载页面。重启/重载后走上述启动流程。

WASM 运行时语言切换的两个必要条件:其一,SeatFlow.Browser.csproj 设置 <UseSystemResourceKeys>false</UseSystemResourceKeys>(WASM 发布默认为 true,会剔除 .resx 卫星资源);其二,wwwroot/main.js 创建运行时加 .withConfig({ loadAllSatelliteResources: true })(独立 WASM 的按需解析无法定位 _framework/{culture}/ 下的资源文件,需预加载注册卫星程序集)。缺失任一项时 en-US 会永远回退中文。验证与排障见 docs/WebDeployment.md。

策略内部 i18n

策略 manifest 中的用户可见文字使用内联 i18n 词典格式,而非 .resx 键:

{
  "label": { "zh-CN": "历史窗口大小", "en-US": "History Window Size" },
  "messages": {
    "DeskMate_Split": {
      "zh-CN": "同桌组({0})中的 {1} 已被前排策略分配",
      "en-US": "Desk-mate group ({0}) member(s) {1} already assigned"
    }
  }
}

LocalizeHelper.Resolve

Presentation 层通过 LocalizeHelper.Resolve(dict) 方法解析:

public static string Resolve(Dictionary<string, string> localizedDict)
{
    if (localizedDict.TryGetValue(CultureInfo.CurrentUICulture.Name, out var value))
        return value;
    if (localizedDict.TryGetValue("zh-CN", out var fallback))
        return fallback;
    return localizedDict.Values.FirstOrDefault() ?? "";
}

解析顺序:

  1. 精确匹配 CultureInfo.CurrentUICulture.Name(如 en-US)
  2. 回退到 zh-CN
  3. 回退到第一个可用值

内置策略的消息模板在 SeatFlow.Core/Strategies/Manifests/{Id}.json 中声明。

i18n.py 脚本工具

scripts/i18n.py 统一管理三文件(zh-CN .resx、en-US .resx、Designer.cs)的增删改查和同步。所有命令从 scripts/ 目录执行。

常用命令

cd scripts

# 列出所有键
python3 i18n.py list

# 查找未翻译的键(zh-CN == en-US)
python3 i18n.py list --missing-en

# 搜索匹配特定模式的键
python3 i18n.py list --pattern "Export"

# 列出含格式占位符的键
python3 i18n.py list --format-strings

# 按分类过滤
python3 i18n.py list --category Settings

# 导出为 JSON 或 CSV
python3 i18n.py list --output json
python3 i18n.py list --output csv > translations.csv

校验

# 一致性校验
python3 i18n.py check

# 自动修复排序问题
python3 i18n.py check --fix

校验项包括:

  • 三文件 key 集合一致性
  • key 命名规范(Category_MeaningfulName PascalCase)
  • zh-CN 和 en-US 格式字符串参数数量匹配
  • 空值检测
  • XML 重复 key 检测

增删改查

# 添加新键
python3 i18n.py add Settings_NewOption --zh "新选项" --en "New Option"
python3 i18n.py add Common_NewAction --zh "执行操作" --en "Execute" --comment "工具栏按钮"

# 修改已有键
python3 i18n.py modify Settings_Title --zh "系统设置"
python3 i18n.py modify Settings_Title --en "System Settings"

# 重命名键(三文件同步)
python3 i18n.py rename Seating_Title Seating_WindowTitle

# 删除键
python3 i18n.py delete Obsolete_Key
python3 i18n.py delete Temp_DebugKey --force

Designer.cs 同步

# 预览变更
python3 i18n.py sync --dry-run

# 执行同步
python3 i18n.py sync

从 Resources.resx 的 key 列表重新生成 Resources.Designer.cs 中的所有属性,保留原有注释分隔符和代码结构。

批量翻译工作流

# 1. 导出为 CSV
python3 i18n.py export -o translations.csv

# 2. 在 Excel 中编辑 translations.csv

# 3. 预览导入
python3 i18n.py import translations.csv --dry-run

# 4. 执行导入
python3 i18n.py import translations.csv --force

安全机制

机制 说明
自动备份 所有写入操作自动备份三文件到 Lang/.backup/
Dry-run --dry-run 展示变更摘要但不实际写入
确认提示 默认要求输入 y 确认;--force 跳过
原子性 先完成全部校验,再一次性写入所有文件
XML 合法性 写入后立即重新解析验证 XML 格式

常见工作流

添加新的 UI 文案:

python3 i18n.py add Settings_AutoSave --zh "自动保存" --en "Auto Save"
# 在 C# 中使用:StatusMessage = Resources.Settings_AutoSave;
# 在 AXAML 中使用:<TextBlock Text="{x:Static lang:Resources.Settings_AutoSave}" />
python3 i18n.py check
dotnet build

批量修改翻译:

python3 i18n.py export -o translations.csv
# 在 Excel 中编辑 CSV
python3 i18n.py import translations.csv --dry-run
python3 i18n.py import translations.csv
python3 i18n.py check
dotnet build

用户自定义语言配置

语言通过 AppSettings.Language 存储,值为区域代码(如 zh-CN、en-US)。用户在设置页面选择语言后:

  1. AppSettings.Language 持久化到 JSON(保存时提示重启/重新加载)
  2. 桌面重启后 ApplyLanguageFromSettings() 读取并应用;Web 重新加载后由 Program.Main 预加载
  3. 所有 {x:Static} 绑定和 Resources.xxx 调用自动切换到目标语言

资源文件结构参考

完整脚本参考文档见 scripts/ToolsCollection.md。脚本单元测试在 scripts/tests/test_i18n.py(45 个测试用例)。