RPC远程过程调用,二进制封装,提供高吞吐低延迟的高性能RPC框架
|
# 多语言本地化(L.T)
本文档说明 NewLife.Remoting 两个包如何接入 NewLife.Core v12 的运行时本地化能力:英文原文为词表键、`Locales/*.yml` 提供译文、`L.T(...)` 在抛异常处显式翻译,日志由 `Logger.Format` 自动翻译。
## 1. 设计取舍
| 项 | 选择 | 理由 |
|---|------|------|
| 词表键 | **英文原文** | 未提供译文的语言自动回退英文,接入成本为零;键即文案,不存在"键名与文案两层维护" |
| 译文载体 | `Locales/*.yml`(编译期源生成) | 不依赖 `ResourceManager` 与卫星程序集,NativeAOT 下可用,词表以 Deflate 压缩内嵌 |
| 译文语言 | 内置 31 种语言,与 NewLife.Core 词表集合保持一致 | 简体 `zh-Hans`、繁体 `zh-Hant`(统一承接 `zh-TW` / `zh-HK` / `zh-MO`)为主维护语言,其余 29 种语言随版本同步;未列出的区域性回退英文 |
| 翻译入口 | 异常 `L.T(...)`;日志直接写英文模板 | `Logger.Format` 已统一翻译日志,`XException` 构造器已统一翻译自定义异常 |
| 不翻译的内容 | `[Justification]`/XML 注释、表结构元数据(XCode 模型的字段描述)、设备历史记录文本、协议报文与结构化日志 | 元数据与持久化数据应保持稳定,随写入时的语言漂移会让历史记录前后不一致;`[{0}]=>{1}`、`commandId=...`、统计表格等结构化日志不承载自然语言 |
| 元数据标签 | `[DisplayName]`/`[Description]` 用**英文源文**,由展示层(Cube 的枚举/控制器渲染、`Cluster mode: {0}` 等)调用 `L.T` 翻译;与消息词条一样要求 **31 种语言全量覆盖** | 同一标签可能被日志、管理界面、协议元数据多处消费;源文英文保证标识稳定,翻译下沉到渲染层,词表缺译文由门禁拦截而不是静默回退 |
## 2. 目录与文件
```
NewLife.Remoting\Locales\{ar,bn,cs,da,de,el,es,fa,fi,fr,he,hi,hu,id,it,ja,ko,ms,nb,nl,pl,pt,ro,ru,sv,th,tr,uk,vi,zh-Hans,zh-Hant}.yml
NewLife.Remoting.Extensions\Locales\{同上 31 种}.yml
```
- 每个组件维护自己的 31 份词表,模块初始化时自动 `Localization.Register`;类库默认**不覆盖** Core 同名译文。
- 语言集合与 NewLife.Core 的 `Locales/` 完全对齐,下游项目引用本库后无需再补语言文件。
- 引用 `NewLife.Core` 包时,包内 `build/NewLife.Core.props` 会把 `Locales/*.yml` 自动挂成 `AdditionalFiles`,**无需改 csproj**。
- 词表格式为逐行 `"英文原文": "译文"`,键必须与源码字符串**逐字一致**(含空格与标点)。
- 程序集标题与描述写进 csproj 的 `AssemblyTitle`/`Description`,`[DisplayName]`/`[Description]` 特性同理,都以英文源文为词表键并在**所有 31 种语言**中提供译文;`PackageDescription` 保留中英混排描述供 NuGet 展示,NuGet 元数据不随界面语言变化。
## 3. 编码约定
```csharp
// 自定义异常:XException 构造器自动翻译,直接写英文
throw new XException("Session {0} is closed.", sessionId);
// BCL 异常与 ApiException:显式翻译
throw new ApiException(ApiCode.NotFound, L.T("Cannot find service named [{0}]!", action));
throw new InvalidOperationException(L.T("Streaming return type {0} does not implement IAsyncEnumerable<T>", type));
// 日志:写英文模板与参数,Logger.Format 自动翻译(不要用字符串插值)
Log?.Error("Login failed: {0}", ex.Message);
WriteLog("Update found: {0}", info.ToJson(true));
// 带实例前缀:使用 Core 的 *Prefixed 扩展,前缀不参与翻译
Log?.ErrorPrefixed("[" + Name + "]", "Login failed: {0}", ex.Message);
Log?.WritePrefixed(LogLevel.Debug, "[" + Name + "]", "WebSocket connection closed [{0}]", errorCode);
// 埋点标签:AppendTag 只接受单参数,需显式翻译
span?.AppendTag(L.T("Session [{0}] not found, cannot process the message.", code));
```
**禁止字符串插值**(`$"..."`):模板在进入日志/翻译前已被展开,无法再查表。需要格式化时一律写成"模板 + 参数"。
**名称前缀不进入词表键**:带实例前缀的日志统一使用 NewLife.Core 的 `InfoPrefixed` / `ErrorPrefixed` / `WritePrefixed` 扩展,内部先按 `LogCulture` 翻译并格式化消息,再拼接前缀。`ApiHost`、`ClientBase`、`Upgrade`、`ClientSingleCluster`、`ClientPoolCluster` 的 `WriteLog` 包装器均走这条路径。新增同类日志时不要写成 `$"[{Name}]{format}"`,也不要把 `[{0}]` 前缀放进格式串的占位符,否则前缀会进入词表键,导致查表失败或译文携带实例信息。
**自定义宿主接入请求语言**:在请求入口创建作用域即可复用同一套解析逻辑(q 权重、非法语言忽略、释放还原):
```csharp
using var culture = RequestCultureScope.Create(context.Request.Headers.AcceptLanguage.ToString());
```
> 直接使用 NewLife.Core 的 `NewLife.RequestCultureScope`;Remoting 不再本地维护该类型。
## 4. 校验与门禁
```powershell
# 语言清单(locales.json,与 NewLife.Core 对齐)+ 词表键集合、占位符、格式说明符、
# 未翻译(句子级词条的译文不得与英文原文相同)、
# 孤儿键(按 .cs 完整字符串字面量与 .csproj 元素值匹配,覆盖程序集标题/描述)、
# 缺少词条(源码显式调用 L.T / L.TLog / Localization.Translate(ForLog) 的字面量必须在基准词表)
# 运行时消息覆盖(日志/异常/控制台/事件调用中的自然语言字面量必须在基准词表)
# 全部语言必须同键集,不设可选键豁免;清单缺失/重复、语言数低于 minimumLocales 或译文为空直接失败
pwsh -File Tools/Scripts/check-locales.ps1
# 运行时消息不得含中文(注释与特性参数不受限)
pwsh -File Tools/Scripts/check-english-exceptions.ps1
# 盘点模式(不失败),用于新增文案后普查
pwsh -File Tools/Scripts/check-english-exceptions.ps1 -List
```
两条命令都在 `.github/workflows/test.yml` 的 `build` 作业中执行,任一失败即阻断合并。
## 5. 测试约定
- 测试进程在 `TestSetup` 中固定 `Localization.Culture = en-US`,断言直接针对英文原文,避免机器区域设置影响结果。
- `LocalizationTests` 覆盖简体、繁体、德语、英文回退与全部 31 种语言的词表/日志模板翻译,并放入 `GlobalState` 串行集合,避免与并行用例互相污染。
- 新增译文时同步补一条断言,确保词表真的被源生成器打包进程序集。
## 6. 新增词条的流程
1. 源码中把中文消息改为英文模板;异常用 `L.T`,日志用模板 + 参数。
2. 在对应组件 `Locales/` 下的 31 个语言文件各加一行,键为英文原文;**所有词条(含程序集标题/描述与 `[DisplayName]`/`[Description]` 元数据)都必须在 31 种语言中全量提供**。句子级词条必须给出实际译文(`check-locales.ps1` 会拦截与英文原文相同的占位译文),单词型技术词(如 `MD5: {0}`)可沿用目标语言的通行写法。
3. 运行 `pwsh -File Tools/Scripts/check-locales.ps1` 与 `Tools/Scripts/check-english-exceptions.ps1` 确认通过。
4. 编译后词表在模块初始化时注册;缺译文的语言按英文回退,不会抛异常。
## 7. 已知边界
- **设备历史记录**(`WriteHistory` 的标签与明细)按数据对待,仍为中文;如需多语言展示,应由读侧按标签码翻译,而不是写入时按当前语言落库。
- **协议报文与结构化日志**(`[{0}]=>{1}`、`commandId=...`、统计表格等)不含自然语言,保持英文/符号形式,不进入词表。
- **异常动作前缀**:客户端重新抛出异常时会拼接 `[action]` 前缀(如 `[Login]...`),前缀是调用标识、不进入词表;内部异常消息已在抛出点按当前语言翻译。
- **服务端响应语言**:`RequestCultureScope` 会按 `Accept-Language` 的 q 权重切换当前 UI 文化,请求结束自动还原。HTTP 入口由 NewLife.Core 的 `HttpServer` 统一处理(`ApiHttpHandler` 不再重复创建作用域),最小 API 端点与 MVC `[ApiFilter]` 显式创建作用域。直接使用 `NewLife.Core` 的 `NewLife.RequestCultureScope`,按“最高权重且存在译文表”的语言选择(最高权重语言不支持时退守下一个受支持语言,全部不支持时保持宿主语言)。宿主已配置 ASP.NET Core `RequestLocalization` 时,`[ApiFilter]` 与最小 API 端点检测到 `IRequestCultureFeature` 后直接采用宿主选择;未配置时继续使用内置解析。宿主仍可全局固定 `Localization.Culture`(此时请求头不生效);不要在并发服务中临时修改全局 `Localization.Culture`。HTTP 客户端发送 `Accept-Language` 即可指定响应语言;SRMP/TCP 没有标准语言协商,客户端应以 `ApiException.Code` 为准自行本地化,或由业务在登录/请求中携带语言。**.NET Framework 目标不做请求级切换**(`CurrentUICulture` 为线程静态,异步请求间可能串语言),保持宿主语言。
- **日志语言**:默认跟随 `Localization.Culture` 或当前 UI 文化;开启请求语言协商后,建议设置 `Localization.LogCulture` 固定宿主语言,避免同一进程的日志随请求语言漂移。`*Prefixed` 扩展与 `Logger.Format` 都优先读取 `LogCulture`。
- **`InvariantGlobalization=true`** 下具名区域性不可用,源生成器会跳过注册,运行时全部回退英文。
- 词表随程序集编译,新增语言需要重新发版;运行时临时补充语言可用 `Localization.Register(culture, table)`。
## 8. 收口记录
以下为本仓多语言审计中**已决策不处理**的项,后续扫描不再重复登记;功能清单的「收口记录」章节索引本节。
| 项 | 位置 | 处置与理由 |
|---|---|---|
| `MD5: {0}`、`Encoder: {0}`、`Handler: {0}` 在部分语言保留英文,`Failover` 在部分语言沿用借词 | `NewLife.Remoting/Locales/*.yml` | 均为单词型技术标签,目标语言的 IT 惯用写法即原文(德语 `Failover`、荷兰语 `Failover` 等);`check-locales.ps1` 只对句子级词条(≥3 个片段)做未翻译拦截 |
| 表结构元数据(XCode 模型字段描述)保持母语 | 各 `Model.xml` | 表结构元数据不是运行时消息,翻译会让历史数据与库结构前后不一致 |
| 设备历史记录标签与明细保持中文 | `DefaultDeviceService.WriteHistory` | 按持久化数据对待,展示层按标签码翻译 |
| `PackageReleaseNotes` 仍为中文 | 各 csproj | 发版流程专属字段,日常提交不改动,发版时统一整理 |
| `XException` 未纳入「缺少词条」校验 | `Tools/Scripts/check-locales.ps1` | 构造器已统一翻译;`L.T`/`Localization.Translate` 是显式本地化入口,作为门禁更精准 |
|