☰
Humanizer 本地化格式化核心:DefaultFormatter 类实现原理与扩展指南
2026/9/29 7:29:07 网站建设 项目流程
  • 开发工具

【免费下载链接】Humanizer

Humanizer meets all your .NET needs for manipulating and displaying strings, enums, dates, times, timespans, numbers and quantities

项目地址:https://gitcode.com/gh_mirrors/hu/Humanizer
点击查看免费下载

DefaultFormatter是 Humanizer 中IFormatter接口的默认实现,负责将日期、时间跨度、时间单位与数据单位按指定区域文化(Culture)渲染为本地化文本。本文以官方 API 文档 Humanizer.DefaultFormatter.md 为主体,结合仓库源码逐项解析其构造函数、公开方法、可扩展点与数据驱动架构,帮助你理解 Humanizer 多语言格式化机制,并掌握如何为自定义区域文化接入或替换格式化逻辑。

一、类概览:DefaultFormatter 在 Humanizer 中的定位

public class DefaultFormatter : Humanizer.IFormatter

从 API 文档给出的声明可以看出,DefaultFormatter继承自System.Object,并实现Humanizer.IFormatter接口。它在 Humanizer 的本地化体系中的职责是:把"时间单位 + 数量 + 时态"等结构化输入,翻译成符合当前区域文化习惯的自然语言短语,例如英语的 "3 days ago"、俄语的 "3 дня назад"。

在配置层面,DefaultFormatter是Configurator.Formatters注册表的默认兜底实现。源码 FormatterRegistry.cs 中可以看到:

class FormatterRegistry : LocaliserRegistry<IFormatter> { public FormatterRegistry() : base(c => new DefaultFormatter(c)) => FormatterRegistryRegistrations.Register(this); }

即:当某个区域文化没有注册专门的格式化器时,LocaliserRegistry<IFormatter>会直接以new DefaultFormatter(culture)兜底。而 Configurator.cs 通过public static LocaliserRegistry<IFormatter> Formatters { get; } = new FormatterRegistry();将这一注册表暴露为全局配置入口。

二、构造函数:两种创建方式与区域文化解析

API 文档定义了两种构造函数:

public DefaultFormatter(string localeCode); public DefaultFormatter(System.Globalization.CultureInfo culture);

对应源码 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)) { }

要点如下:

  • localeCode构造方式:直接传入区域代码字符串,例如"ru-RU"、"de",内部会先转换为CultureInfo再委托给另一个构造函数。
  • culture构造方式:接收完整的CultureInfo实例,例如CultureInfo.GetCultureInfo("en")。
  • 短语表解析:构造函数通过LocalePhraseTableCatalog.Resolve(culture)解析出该区域文化的生成式短语表(LocalePhraseTable),如果解析失败(连英语兜底表都缺失)则抛出InvalidOperationException。

LocalePhraseTableCatalog.Resolve的解析策略在 LocalePhraseTable.cs 中实现:它会从当前文化沿Parent链向上回溯(例如zh-CN→zh),逐步尝试解析;找不到任何匹配时最终回退到"en"英语表。这意味着任何未显式覆盖的区域文化都会得到英语短语作为最后的兜底。

三、Culture 属性:格式化使用的区域文化

protected System.Globalization.CultureInfo Culture { protected get; }

Culture是一个protected属性,仅在类内部及派生类中可读。它在构造函数中被赋值,用于:

  • 解析数字词的本地化形式(NumberToWords);
  • 决定数字的格式化方式(如number.ToString(Culture));
  • 作为生成式短语表解析的输入。

对派生类作者而言,Culture是感知"当前格式化器服务于哪种语言"的主要入口。

四、日期相对化方法:Now / Never / 相对时间

4.1 DateHumanize_Now 与 DateHumanize_Never

public virtual string DateHumanize_Now(); public virtual string DateHumanize_Never();

这两个无参方法分别返回"此刻"与"从不"的本地化文本。从源码看,它们直接读取短语表并提供默认值:

public virtual string DateHumanize_Now() => phraseTable.DateNow ?? "now"; public virtual string DateHumanize_Never() => phraseTable.DateNever ?? "never";

也就是说,英语区域默认输出now/never;俄语区域则输出сейчас/никогда(见 ru.yml 中now: 'сейчас'、never: 'никогда'的配置)。

4.2 DateHumanize(TimeUnit, Tense, int):相对日期短语

public virtual string DateHumanize(Humanizer.TimeUnit timeUnit, Humanizer.Tense timeUnitTense, int unit);

这是日期相对化("2 days ago" / "in 3 hours")的核心方法,参数含义为:

  • timeUnit:时间单位,来自TimeUnit枚举(Millisecond、Second、Minute、Hour、Day、Week、Month、Year);
  • timeUnitTense:时态,来自Tense枚举(Past 过去 / Future 将来);
  • unit:单位数量。

源码实现(DefaultFormatter.cs)为:

public virtual string DateHumanize(TimeUnit timeUnit, Tense timeUnitTense, int unit) => TryFormatDateFromPhraseTable(timeUnit, timeUnitTense, unit, out var result) ? result : throw new InvalidOperationException($"Missing generated relative-date phrase for '{Culture.Name}' and unit '{timeUnit}'.");

内部逻辑(TryFormatDateFromPhraseTable)遵循以下优先级:

  1. 数量为 0 时直接返回"此刻"短语;
  2. 数量为 1 且存在单数形式时返回Single短语(例如俄语миллисекунду назад);
  3. 数量为 2 且存在名为two的精确模板时使用双数模板;
  4. 否则按数量选择单数 / 双数 / 少数 / 复数等语法形式,并通过{count}占位符渲染出完整短语。

五、时间跨度方法:零值、常规值与年龄表达

5.1 TimeSpanHumanize_Zero:零时长表示

public virtual string TimeSpanHumanize_Zero();

文档明确说明其语义为"0 seconds"(零秒的字符串表示)。源码读取短语表的TimeSpanZero字段,默认值为"no time":

public virtual string TimeSpanHumanize_Zero() => phraseTable.TimeSpanZero ?? "no time";

俄语区域的配置在 ru.yml 中为zero: 'нет времени'。

5.2 TimeSpanHumanize(TimeUnit, int, bool):常规时长格式化

public virtual string TimeSpanHumanize(Humanizer.TimeUnit timeUnit, int unit, bool toWords=false);

参数说明:

  • timeUnit:要表示的时间单位;
  • unit:单位数量;
  • toWords:false(默认)时数量以数字呈现,true时数量以单词呈现(如 "three hours" 而非 "3 hours")。

注意:3.0.10 版本 API 文档标注该方法在timeUnit大于TimeUnit.Week时会抛出ArgumentOutOfRangeException,即文档时代该方法的合法输入范围为毫秒到周。当前仓库源码已重构为数据驱动实现(DefaultFormatter.cs),当短语表缺少对应短语时抛出InvalidOperationException,且不再限制在周以内——使用时应以你所引用版本的实际行为为准。

源码中的词形选择逻辑(FormatTimeSpanPhrase)会依据toWords在数字变体与单词变体之间切换(SingleWordsVariant/MultipleWordsVariant),并把数量渲染为数字或本地化数字词。

5.3 TimeSpanHumanize_Age:年龄后缀格式

public virtual string TimeSpanHumanize_Age();

文档给出的示例非常直观:英语中该方法返回把时长变成年龄表达的格式,"40 years"通过添加" old"后缀变成"40 years old"。源码实现为:

public virtual string TimeSpanHumanize_Age() { return phraseTable.TimeSpanAge ?? "{0}"; }

它返回的是一个格式模板({0}占位符),由调用方把已人性化的时长文本填入。俄语区域在 ru.yml 中将其配置为template: '{value}'(无后缀)。

六、单位方法:时间单位符号与数据单位

6.1 TimeUnitHumanize(TimeUnit):时间单位符号

public virtual string TimeUnitHumanize(Humanizer.TimeUnit timeUnit);

该方法返回给定时间单位的本地化符号(例如英语的s、m、h、d等)。源码从短语表的时间单位条目中取Symbol字段:

public virtual string TimeUnitHumanize(TimeUnit timeUnit) { if (phraseTable.TryGetTimeUnitPhrase(timeUnit, out var generatedPhrase) && generatedPhrase.Symbol is { } generatedSymbol) { return generatedSymbol; } throw new InvalidOperationException($"Missing generated time-unit phrase for '{Culture.Name}' and unit '{timeUnit}'."); }

6.2 DataUnitHumanize(DataUnit, double, bool):数据单位格式化

public virtual string DataUnitHumanize(Humanizer.DataUnit dataUnit, double count, bool toSymbol=true);

文档对参数的解释:

  • dataUnit:数据单位;
  • count:单位数量,用于调整单复数形式;
  • toSymbol:true(默认)时以符号形式表达数据单位,false时以完整单词表达。

源码实现(DefaultFormatter.cs)有一个值得注意的细节——英语兜底回退:

public virtual string DataUnitHumanize(DataUnit dataUnit, double count, bool toSymbol = true) { if (TryFormatDataUnitFromPhraseTable(dataUnit, count, toSymbol, out var generated)) { return generated; } if (dataUnit is DataUnit.Petabyte or DataUnit.Exabyte or DataUnit.Pebibyte or DataUnit.Kibibyte or DataUnit.Mebibyte or DataUnit.Gibibyte or DataUnit.Tebibyte && !Culture.Name.Equals("en", StringComparison.OrdinalIgnoreCase)) { return EnglishFallback.DataUnitHumanize(dataUnit, count, toSymbol); } throw new InvalidOperationException($"Missing generated>protected virtual string Format(Humanizer.TimeUnit unit, string resourceKey, int number, bool toWords=false); protected virtual string Format(string resourceKey); protected virtual string GetResourceKey(string resourceKey); protected virtual string GetResourceKey(string resourceKey, int number);

文档对Format(TimeUnit, string, int, bool)的说明是"格式化指定的资源键",且当指定文化下资源不存在时抛出ArgumentException。对GetResourceKey(string, int)的说明是:"如果你的区域文化围绕多单位有复杂规则,请重写此方法,例如阿拉伯语、俄语"——这正是 3.x 早期版本支持阿拉伯语双数(2 天为 يومين)、俄语格变化等特性的机制所在。

需要特别说明的是:当前仓库主分支源码中,这些基于"资源键拼接"的扩展点已被数据驱动架构取代。现在由ProfiledFormatter(ProfiledFormatter.cs,继承自DefaultFormatter)承担区域定制逻辑,它通过声明式的FormatterProfile记录词形检测器(FormatterNumberDetectorKind)、精确数字规则(FormatterDateFormRule/FormatterTimeSpanFormRule)、介词模式(如罗马尼亚语de)、性别表(UnitGenders)等,实现了同一套格式化内核、按区域数据差异化输出的效果。NumberToWords仍保留为 protected 扩展点:

protected virtual string NumberToWords(TimeUnit unit, int number, CultureInfo culture) => number.ToWords(culture);

ProfiledFormatter对其进行了性别感知覆盖——当配置了UnitGenders时调用number.ToWords(gender, culture)。这也解释了为什么俄语 ru.yml 中要为每个时间单位声明timeUnitGenders(如hour: 'masculine'、second: 'feminine')。

八、语法形式解析:单数、双数、少数与复数

DefaultFormatter之所以能输出正确的本地化短语,依赖LocalePhraseTable中按语法形式存储的多套文本。数据结构定义于 LocalePhraseTable.cs:

readonly record struct LocalizedPhraseForms( string Default, string? Zero = null, string? Singular = null, string? Dual = null, string? Paucal = null, string? Plural = null, string? Many = null);

而ProfiledFormatter.DetectNumberForm(ProfiledFormatter.cs)实现了多种语言的数字→语法形式映射:

  • SingularPlural:1 用单数,其余用复数(英语风格);
  • ArabicLike:1 单数、2 双数、3–10 复数;
  • ArabicCardinal:0 零、1 单数、2 双数、%1003–10 复数、%10011–99 多数;
  • Between2And4Paucal:1 单数、2–4 少数;
  • Polish:以 2–4 结尾且非 12–14 时用少数;
  • SouthSlavic:塞尔维亚/克罗地亚式单数、少数规则;
  • Slovenian:单数、双数、少数(3、4);
  • Russian:调用RussianGrammaticalNumberDetector;
  • Lithuanian:调用LithuanianNumberFormDetector。

短语表本身由源生成器根据 Locales 目录下的 YAML 文件在编译期生成(LocalePhraseTableCatalog.ResolveCore为源生成器实现的分部方法),运行时仅做查表与模板渲染,从而保持轻量。俄语 ru.yml 中past.millisecond的forms配置(default/singular/dual)即是对上述多形式短语数据的直观示例。

九、实战:注册与替换自定义格式化器

在实际项目中,DefaultFormatter通常不需要直接实例化——Humanizer 的扩展方法(如TimeSpan.Humanize()、DateTime.Humanize())会通过 Configurator.cs 的GetFormatter(culture)自动解析当前线程区域文化对应的格式化器:

internal static IFormatter GetFormatter(CultureInfo? culture) => Formatters.ResolveForCulture(culture);

如果需要为特定区域文化注入自定义格式化逻辑,可以通过Configurator.Formatters注册表覆盖默认实现(注册表基类LocaliserRegistry<T>按文化名解析),例如在应用启动时:

Configurator.Formatters.Register<MyLocaleCode, MyFormatter>();

其中MyFormatter可以继承DefaultFormatter并重写DateHumanize、TimeSpanHumanize、TimeUnitHumanize等virtual方法,也可以直接实现IFormatter接口。需要注意:当前源码中若自定义格式化器需要支持语法格感知的时长(IGrammaticalCaseTimeSpanFormatter),必须显式实现该接口,否则会抛出NotSupportedException(见 DefaultFormatter.cs)。

十、小结

DefaultFormatter是 Humanizer 本地化体系的中枢实现:它以CultureInfo为输入,以生成式短语表为数据源,统一承载日期相对化、时长人性化、时间单位符号与数据单位符号的本地化输出;同时通过virtual方法保留了派生扩展能力。从 3.0.10 的"资源键 +GetResourceKey重写"模式演进到当前主分支的"声明式FormatterProfile+ 源生成短语表"模式,其核心目标始终如一——让新增语言的成本从"写 C# 代码"降为"补充 YAML 数据"。理解DefaultFormatter,就等于掌握了 Humanizer 多语言输出的底层逻辑与扩展路径。

  • 开发工具

【免费下载链接】Humanizer

Humanizer meets all your .NET needs for manipulating and displaying strings, enums, dates, times, timespans, numbers and quantities

项目地址:https://gitcode.com/gh_mirrors/hu/Humanizer
点击查看免费下载
上一篇:DDrawCompat 使用教程:免费修复 DirectDraw 老游戏,Win10/11 上稳定满 60 帧
下一篇:Video Subtitle Master 1.4.0 版本解析:翻译通道扩展与 Core ML 加速,批量字幕工作流升级指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询