- 开发工具
【免费下载链接】Humanizer
Humanizer meets all your .NET needs for manipulating and displaying strings, enums, dates, times, timespans, numbers and quantities
本指南以 Humanizer 3.x 版本 API 文档中的IFormatter接口(原文档)为主体,结合仓库内src/Humanizer的源码实现,系统讲解该接口的完整方法契约、参数语义、默认实现原理,以及如何通过Configurator与LocaliserRegistry注册自定义格式化器。读完本文,你将掌握 Humanizer 本地化格式化体系的内部结构,并能独立为缺少内置支持的语种编写、注册与测试IFormatter实现。
为什么需要 IFormatter:复数与语法规则差异的抽象
Humanizer 负责把数字、日期、时间跨度、数据单位等值转换为人类可读的字符串。不同语言处理“数量 + 单位”的方式差异巨大,远不止加不加s这么简单:
- 罗马尼亚语中 “5 days” 是 “5 zile”,而 “24 days” 是 “24 de zile”,需要根据数值决定是否插入介词
de; - 阿拉伯语中 “2 days” 是双数形式
يومين,而不是2 يوم; - 俄语、波兰语、斯洛文尼亚语等语言还存在 paucal(少数)、dual(双数)、many(多数)等额外的语法数范畴。
IFormatter正是为此设计的抽象层。接口的定义位于 src/Humanizer/Localisation/Formatters/IFormatter.cs,其职责注释明确写着:
Localizes Humanizer's number, date, duration, and unit formatting.
它把“如何用某种语言表达某个时间单位/数据单位”的规则全部封装为一个契约,使上层扩展方法(如Humanize、TimeSpanHumanize、ToSymbol)不关心具体语言的语法细节,只面向接口编程。
接口的派生类型为DefaultFormatter,完整实现见 src/Humanizer/Localisation/Formatters/DefaultFormatter.cs,它是所有语言格式化器的基类,下面逐一拆解其成员。
接口成员全解
IFormatter共声明 8 个方法,覆盖四类场景:相对日期、时间跨度(时长)、数据单位、时间单位符号。接口定义如下:
public interface IFormatter { string DateHumanize_Now(); string DateHumanize_Never(); string DateHumanize(TimeUnit timeUnit, Tense timeUnitTense, int unit); string TimeSpanHumanize_Zero(); string TimeSpanHumanize(TimeUnit timeUnit, int unit, bool toWords = false); string TimeSpanHumanize_Age(); string DataUnitHumanize(DataUnit dataUnit, double count, bool toSymbol = true); string TimeUnitHumanize(TimeUnit timeUnit); }所有方法返回string,即某种语言下的最终可读文本。方法命名中的_Now、_Never、_Zero、_Age等后缀用于区分类似但语义不同的短语(如“现在”与“从未”、“0 秒”与“年龄后缀”),避免与带参数的DateHumanize/TimeSpanHumanize重载冲突。
相对日期短语:DateHumanize 家族
| 方法 | 签名 | 语义 |
|---|---|---|
DateHumanize_Now | string DateHumanize_Now() | 返回“此刻/现在”的本地化文本,如英文now |
DateHumanize_Never | string DateHumanize_Never() | 返回“从未发生”的本地化文本,如英文never |
DateHumanize | string DateHumanize(TimeUnit timeUnit, Tense timeUnitTense, int unit) | 返回指定时间单位、时态与数量的相对日期短语 |
其中DateHumanize的三个参数含义如下:
timeUnit:TimeUnit枚举(src/Humanizer/Localisation/TimeUnit.cs),取值包括Millisecond、Second、Minute、Hour、Day、Week、Month、Year;timeUnitTense:Tense枚举(src/Humanizer/Localisation/Tense.cs),Future表示“in 2 days”这类未来表达,Past表示“2 days ago”这类过去表达;unit:单位数量,用于驱动单数/双数/复数等语法形态的选择。
从源码看,DateHumanize_Never被空值日期的Humanize扩展直接调用。在 src/Humanizer/DateHumanizeExtensions.cs 中,DateTime?为null时通过Configurator.GetFormatter(culture).DateHumanize_Never()返回never或对应语言的等价文本。
DateHumanize的默认实现会先在短语表中查“count == 0 即 now”的情况,再处理single(数量为 1)、two模板(阿拉伯语“前天/后天”类特殊双数表达)与multiple(其余数量)分支,详见 DefaultFormatter.cs。
时间跨度短语:TimeSpanHumanize 家族
| 方法 | 签名 | 语义 |
|---|---|---|
TimeSpanHumanize_Zero | string TimeSpanHumanize_Zero() | 返回零时长文本,如英文0 seconds |
TimeSpanHumanize | string TimeSpanHumanize(TimeUnit timeUnit, int unit, bool toWords = false) | 返回时长短语;toWords为true时数字转英文单词 |
TimeSpanHumanize_Age | string TimeSpanHumanize_Age() | 返回“年龄后缀”格式字符串,英文为{0} old |
TimeSpanHumanize的参数中,toWords控制数字的呈现形式。默认实现中,toWords为真时会优先选择SingleWordsVariant/MultipleWordsVariant词形变体,并把计数交给NumberToWords(unit, number, Culture)转成单词(DefaultFormatter.cs)。
TimeSpanHumanize_Age的设计很巧妙:它返回一个带占位符的格式模板,而不是完整句子。英文返回"{0} old",于是 “40 years” 可以被拼装成 “40 years old”。源码实现为phraseTable.TimeSpanAge ?? "{0}"(DefaultFormatter.cs),默认回退为原样输出。
数据单位:DataUnitHumanize
string DataUnitHumanize(Humanizer.DataUnit dataUnit, double count, bool toSymbol = true);dataUnit:DataUnit枚举(src/Humanizer/Localisation/DataUnit.cs),涵盖Bit、Byte、Kilobyte…Exabyte等十进制单位,Pebibyte、Kibibyte…Tebibyte等二进制 IEC 单位,以及DecimalKilobyte…DecimalExabyte、BinaryKibibyte…BinaryPebibyte等显式单位系统取值;count:单位数量,用于调整单复数形态;toSymbol:true时返回符号(如KB、MiB),false时返回完整单词(如kilobyte)。
该方法是数据大小格式化(ByteSize系列)多语言化的关键入口。默认实现会优先查短语表;当目标语言缺少Petabyte、Exabyte及各种二进制单位短语时,会回退到内置英文DefaultFormatter(DefaultFormatter.cs)。
时间单位符号:TimeUnitHumanize
string TimeUnitHumanize(Humanizer.TimeUnit timeUnit);返回给定时间单位的本地化符号,如英文的ms、s、min、h、d、w、M、y。它是TimeSpanHumanize中toSymbols场景(秒的符号化表达)的底层支撑,同时被TimeUnitToSymbolExtensions使用。
默认实现剖析:DefaultFormatter 与短语表机制
DefaultFormatter是IFormatter的唯一直接派生类型(Formatters 目录)。它的构造器接受CultureInfo或 localeCode 字符串,并解析生成好的LocalePhraseTable(DefaultFormatter.cs):
public DefaultFormatter(CultureInfo culture) { Culture = culture; phraseTable = LocalePhraseTableCatalog.Resolve(culture) ?? throw new InvalidOperationException("The generated locale phrase tables are missing the required English fallback."); } public DefaultFormatter(string localeCode) : this(new CultureInfo(localeCode)) { }关键点:
- 短语表由源码生成器生成:各语言短语来自
src/Humanizer/Locales/*.yml(如 ar.yml、en.yml),经Humanizer.SourceGenerators编译为LocalePhraseTable,运行时“分支少、无需解析”; - 每个短语含多种形态:
single、dual、paucal、plural、default等LocalizedPhraseForms字段,以及countPlacement(数字放在单位前/后/不出现)、beforeCountText/afterCountText、template(如阿拉伯语“two”模板)等结构; - 兜底英文:即使某个语种缺短语,也会回退英文(
EnglishFallback,见 DefaultFormatter.cs)。
以阿拉伯语为例,ar.yml 中relativeDate.past.second定义了:
second: single: 'منذ ثانية واحدة' multiple: countPlacement: none forms: default: 'منذ {count} ثانية' singular: 'منذ ثانيتين' dual: 'منذ ثانيتين' paucal: 'منذ ثانيتين' plural: 'منذ {count} ثوان'可见阿拉伯语的“2 秒”直接使用双数短语منذ ثانيتين,而非拼接数字,这正是IFormatter要抽象出的语言差异。
ProfiledFormatter:声明式规则内核
仓库中还存在一个DefaultFormatter的变体ProfiledFormatter(src/Humanizer/Localisation/Formatters/ProfiledFormatter.cs),它从 YAML 中的formatter段读取声明式规则,支持:
- 多种复数检测器:
FormatterNumberDetectorKind枚举定义了SingularPlural、ArabicLike、ArabicCardinal、Between2And4Paucal、Polish、SouthSlavic、Slovenian、Russian、Lithuanian等形态检测算法(ProfiledFormatter.cs); - 精确数字覆盖规则:
FormatterDateFormRule/FormatterTimeSpanFormRule允许为特定数字指定形态(如俄语21, 31…单数、2-4, 22-24…paucal); - 语言特殊处理:罗马尼亚语
de介词插入(ShouldUseRomanianPreposition,当numeral % 100为 0 或大于 19 时插入)、卢森堡语 Eifeler 规则后缀n、拉脱维亚语回退变形等; - 单位性别:
UnitGenders使数字转单词时带上语法性别。
YAML 中的 formatter 配置形如(ar.yml):
formatter: engine: 'profiled' dataUnitFallbackTransform: 'trim-trailing-s' pluralRule: 'arabic-like' casePluralRule: 'arabic-cardinal'由此可推断:ProfiledFormatter是DefaultFormatter针对“可由声明式规则表达”的语言提供的统一实现,新增语种通常只需在 YAML 中补充短语与规则,无需手写 C# 类。
注册与解析机制:Configurator + LocaliserRegistry
IFormatter的实例由Configurator.Formatters注册表统一管理(src/Humanizer/Configuration/Configurator.cs):
public static LocaliserRegistry<IFormatter> Formatters { get; } = new FormatterRegistry();FormatterRegistry(src/Humanizer/Configuration/FormatterRegistry.cs)以DefaultFormatter为默认工厂,然后通过源码生成器批量注册各语种的实现:
class FormatterRegistry : LocaliserRegistry<IFormatter> { public FormatterRegistry() : base(c => new DefaultFormatter(c)) => FormatterRegistryRegistrations.Register(this); }LocaliserRegistry<TLocaliser>(src/Humanizer/Configuration/LocaliserRegistry.cs)提供了解析与注册能力:
ResolveForCulture(CultureInfo? culture):按当前线程文化或指定文化解析格式化器;首次使用时将注册表“冻结”为FrozenDictionary以获得更好的读性能,并用ConditionalWeakTable做按文化的实例缓存(LocaliserRegistry.cs);Register(localeCode, localiser)/Register(localeCode, factory):注册自定义格式化器。注意:注册表被使用(冻结)后不能再注册,会抛出InvalidOperationException(LocaliserRegistry.cs);- 解析时按
culture → culture.Parent链向上回退,最后落到默认格式化器(LocaliserRegistry.cs)。
上层扩展方法通过Configurator.GetFormatter(culture)获取当前文化的IFormatter(Configurator.cs),culture为null时使用当前线程文化。DateHumanizeExtensions中的调用链即为典型示例:
// DateHumanizeExtensions.cs return Configurator .GetFormatter(culture) .DateHumanize_Never();自定义 IFormatter 实战指南
当目标语言无法用 YAML 声明式规则表达,或需要完全自控输出时,可自行实现IFormatter:
1. 定义实现类:继承DefaultFormatter(推荐,可复用短语表机制与英文回退)或直接实现IFormatter全部 8 个方法。若希望支持语法格(grammatical case)感知的时长短语,还应实现可选接口IGrammaticalCaseTimeSpanFormatter(src/Humanizer/Localisation/Formatters/IGrammaticalCaseTimeSpanFormatter.cs),它额外声明:
string TimeSpanHumanize(TimeUnit timeUnit, int unit, GrammaticalCase grammaticalCase);从DefaultFormatter的显式实现可以看出,自定义类型若要启用语法格时长,必须显式实现该接口,否则会抛出NotSupportedException(DefaultFormatter.cs)。
2. 在应用启动时注册:由于注册表一旦使用即被冻结,注册必须发生在任何 Humanizer 调用之前(应用启动阶段或ModuleInitializer中):
Configurator.Formatters.Register("xx-YY", new MyFormatter(new CultureInfo("xx-YY")));3. 验证解析:通过Configurator.Formatters.ResolveForCulture(new CultureInfo("xx-YY"))确认返回的是自定义实例;未注册的文化会沿父文化链回退到DefaultFormatter。
测试与验证
仓库测试目录 tests/Humanizer.Tests 中,与IFormatter直接相关的验证包括:
FormatterExactOutputTests.cs:针对各语种格式化输出做精确断言;Localisation子目录下每个语种文件夹的测试类(如ar/、ru/),逐一验证该语言DateHumanize、TimeSpanHumanize等方法的输出;LocaliserRegistryTests.cs:验证注册表解析、回退与注册机制。
编写自定义IFormatter时,可参照这些测试的模式,用UseCulture特性(tests/Humanizer.Tests/UseCultureAttribute.cs)切换线程文化后断言输出文本。
小结
IFormatter是 Humanizer 本地化体系的枢纽契约:它以 8 个方法覆盖相对日期、时长、年龄后缀、数据单位与时间单位符号五类输出,DefaultFormatter提供了基于生成短语表的默认实现,ProfiledFormatter进一步以声明式规则表达各语言的复数、介词、性别等特殊语法,而Configurator.Formatters注册表则负责按文化解析与自定义扩展。理解这一契约,即可把 Humanizer 的本地化能力延伸到任意语言场景。
- 开发工具
【免费下载链接】Humanizer
Humanizer meets all your .NET needs for manipulating and displaying strings, enums, dates, times, timespans, numbers and quantities
相关推荐
Humanizer 本地化格式化核心:IFormatter 接口与 DefaultFormatter 实现深度解析
Humanizer 本地化格式化核心:IFormatter 接口与 DefaultFormatter 实现深度解析 本指南围绕 Humanizer 的 Huma
开发工具Humanizer 本地化格式化核心接口 IFormatter 完全解析:多语言日期、时长与数据单位的可扩展格式化契约
Humanizer 本地化格式化核心接口 IFormatter 完全解析:多语言日期、时长与数据单位的可扩展格式化契约 IFormatter 是 Humaniz
开发工具Humanizer IFormatter 接口深度解析:多语言日期、时间跨度与数据单位的本地化格式化
Humanizer IFormatter 接口深度解析:多语言日期、时间跨度与数据单位的本地化格式化 IFormatter 是 Humanizer 本地化体系中
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考