date-fns 世界语(Esperanto)locale 深度解析:从 snapshot.md 看 eo 的 format/parse 与相对时间格式化全貌
2026/9/19 18:59:44 网站建设 项目流程

date-fns 世界语(Esperanto)locale 深度解析:从 snapshot.md 看 eo 的 format/parse 与相对时间格式化全貌

【免费下载链接】date-fns⏳ Modern JavaScript date utility library ⌛️项目地址: https://gitcode.com/gh_mirrors/da/date-fns

date-fns 的每个语言包目录下都有一份snapshot.md,它用固定测试数据集记录该 locale 在formatparseformatDistanceformatDistanceStrictformatRelativeformatDuration六个 API 下的完整输出快照。本文以 世界语 locale 的快照 为骨架,逐表解读 eo(Esperanto,ISO 639-2:epo)在 date-fns 中的全部格式化行为与解析回环,并结合 eo locale 源码 说明快照背后的实现机制。读完本文,你将能:理解快照文件的生成与校验方式,掌握 eo 在序数词(-a后缀)、上午/下午标记、柔性日时段、本地化日期时间(P/p系列)等维度上的全部 token 行为,并学会如何阅读任意 locale 的快照来排查本地化问题。

snapshot.md 是什么:一份可测试的本地化契约

在 date-fns 仓库中,每个 locale 目录(如 pkgs/core/src/locale/eo)都包含index.ts_lib/下的五个实现模块,以及这份snapshot.md。快照不是手写文档,而是由构建脚本自动生成的:

  • 生成脚本位于 pkgs/core/scripts/build/localeSnapshots/index.ts,它遍历所有 locale,读取index.ts中的@language注释作为标题,然后依次调用renderFormatParserenderFormatDistancerenderFormatDistanceStrictrenderFormatRelativerenderFormatDuration渲染出五个章节。
  • 脚本强制要求TZ=utc环境(脚本内显式校验process.env.TZ?.toLowerCase() !== "utc"即抛错),确保快照数据与时区无关、可跨机器复现。
  • 对应 npm 脚本定义在 pkgs/core/package.json 中:"locale-snapshots": "env TZ=utc tsx --tsconfig scripts/tsconfig.json -- ./scripts/build/localeSnapshots/index.ts"
  • 脚本支持两种模式:默认generate模式把渲染结果写回各 locale 的snapshot.mdtest模式则读取磁盘上的快照与重新生成的内容比对,不一致即报错——这正是 i18n 贡献指南 中所述"用快照测试 locale"的机制。

因此,snapshot.md既是开发者调试本地化输出的参考手册,也是 CI 中校验 locale 数据是否被意外改动的契约文件。阅读它的前提是理解两列语义:format结果列是把给定日期按 token 格式化后的输出,parse结果列则是把格式化结果再喂回parse得到的日期(即"格式化→解析"回环),用于验证格式化的可逆性与匹配正则的正确性。

format/parse:eo 的全部 token 行为

快照第一个章节覆盖了formatparse的完整 token 矩阵。eo 最显著的语言特征是序数词一律以-a后缀表达(对应世界语形容词词尾,如1-a即"第一"),这在ordinalNumber实现中直接体现:

// pkgs/core/src/locale/eo/_lib/localize/index.ts const ordinalNumber: LocalizeFn<number> = (dirtyNumber) => { const number = Number(dirtyNumber); return number + "-a"; };

凡是以小写o结尾的 token(yoQoMowodoHomoso等)都走这一序数逻辑,输出形如1987-a11-a

年份与季度

Token输入日期format结果parse结果解读
yo(公历纪年序数)1987-02-111987-a1987-01-01纯公历年 +-a
yo0005-01-015-a0005-01-01小年份补零输出(00055-a
Yo(本地周编号年序数)1987-02-111987-a1986-12-29见下方"周起始规则"
Yo0005-01-014-a0003-12-29跨年周回退
Qo / qo(季度序数)2019-01-011-a2019-01-01季度 1 与 2 均验证
QQQ / qqq2019-01-01K12019-01-01缩写季度,数据见quarterValues
QQQQ / qqqq2019-01-011-a kvaronjaro2019-01-01全称季度
QQQQQ / qqqqq2019-01-0112019-01-01窄格式仅数字

Yo的"本地周编号年"行为与 eo 的周配置直接相关。在 eo 入口 中:

options: { weekStartsOn: 1 /* Monday */, firstWeekContainsDate: 4, },

weekStartsOn: 1(周一为一周起点)与firstWeekContainsDate: 4(包含 1 月 4 日的那周为第一周)构成 ISO 风格周制。这正是快照中1987-02-11Yo解析回1986-12-29(1987 年第 6 周的周一)的原因;而0005-01-01落在上一周制年(第 4 周),故Yo输出4-a并解析回0003-12-29

月份(formatting 与 stand-alone)

Mo/Lo输出1-a12-a;缩写(MMM/LLL)、全称(MMMM/LLLL)、窄格式(MMMMM/LLLLL)输出分别来自 localize 中的 monthValues:

宽度输出序列
缩写(MMM/LLL)jan, feb, mar, apr, maj, jun, jul, aŭg, sep, okt, nov, dec
全称(MMMM/LLLL)januaro, februaro, marto, aprilo, majo, junio, julio, aŭgusto, septembro, oktobro, novembro, decembro
窄格式(MMMMM/LLLLL)J, F, M, A, M, J, J, A, S, O, N, D

值得注意的解析回环细节:窄格式月份存在歧义。由于 parse 阶段按parseMonthPatterns.narrow的顺序取第一个命中项(见 match 实现),/^m/会先命中 3 月(marto)而非 5 月(majo),/^j/先命中 1 月(januaro)而非 6/7 月,/^a/先命中 4 月(aprilo)而非 8 月(aŭgusto)。快照因此呈现:

输入(2019 年)MMMMM格式化parse回环结果
01-11J2019-01-01
02-11F2019-02-01
03-11 / 05-10M2019-03-01(两个 M 都解析到 3 月)
04-10 / 08-10A2019-04-01(两个 A 都解析到 4 月)
06-10 / 07-10J2019-01-01(解析到 1 月)
09-10S2019-09-01

这是一个重要的本地化事实:eo 的窄格式月份并不能唯一反解,如果业务上依赖MMMMMparse的双向一致性,需要自行处理歧义。

周、日序数与星期

Token输入日期format结果parse结果
wo(本地周序数)2019-01-011-a2018-12-31
wo2019-12-0148-a2019-11-25
Io(ISO 周序数)2019-01-011-a2018-12-31
Io2019-12-0148-a2019-11-25
do(月内日序数)2019-02-01 / 11 / 281-a / 11-a / 28-a与输入日相同
do MMMM2019-02-1111-a februaro2019-02-11
Do(年内日序数)2019-02-1142-a2019-02-11
Do2019-12-31365-a2019-12-31

星期 token 家族(E/EE/EEE 相同,均输出缩写;EEEE 全称;EEEEE 窄格式;EEEEEE 短格式)与 eo 的dayValues一一对应:全称dimanĉo, lundo, mardo, merkredo, ĵaŭdo, vendredo, sabato,缩写dim, lun, mar, mer, ĵaŭ, ven, sab,窄格式D, L, M, M, Ĵ, V, S,短格式di, lu, ma, me, ĵa, ve, saio(ISO 星期)与eo(本地星期)输出1-a7-a序数(周一为 1),缩写/全称输出与E系列完全一致,因为本地周与 ISO 周在 eo 中起点相同(都是周一)。stand-alone 的co系列与eo系列输出也完全一致。

上午/下午、柔性日时段与时分秒

eo 的上午/下午标记颇具特色:

Token窄/缩写(a, aa, aaa)全称(aaaa)窄窄(aaaaa)
上午a.t.m.antaŭtagmezea
下午p.t.m.posttagmezep

其中a.t.m./p.t.m.分别是世界语antaŭtagmeze/posttagmeze(中午之前/之后)的缩写形式。b系列(AM, PM, noon, midnight)输出与a系列相同。parse 回环中,上午解析到00:00、下午解析到12:00,符合预期。

柔性日时段(B系列)是 eo 快照中最有信息量的部分:

Token11:1314:1319:1302:13
B, BB, BBB, BBBBmatene(早晨)posttagmeze(下午)vespere(傍晚)nokte(夜间)
BBBBBmateneposttagmezevesperenokte

对应的dayPeriodValues定义了五个时段:morning: mateneafternoon: posttagmezeevening: vesperenight: nokte。快照同时揭示了一个边界现象:BBBBB在 14:13(下午时段)时的parse结果为Invalid Date(其余三行均可正常解析)。从 match 的 dayPeriod 正则 看,窄格式posttagmeze可被/^([ap]|(posttagmez|noktomez|tagmez|maten|vesper|nokt)[eo])/i匹配,但parseDayPeriodPatterns.afternoon的正则/^posttagmeze/i与该词元匹配后仍需与柔性日时段解析器协同确定基准时刻,此处回环失败表明 eo 的窄格式柔性日时段解析存在已知限制,业务中应避免对BBBBB输出做parse回环依赖。

小时/分钟/秒的序数 token 输出同样直观:ho([1-12])、Ho([0-23])、Ko([0-11])、ko([1-24])、mo(分钟)、so(秒)均以-a结尾,例如11-a55-a,parse 回环完整。

本地化日期时间(P/p 系列)

P系列与p系列直接映射 formatLong 中的 dateFormats/timeFormats:

Token模板示例(1987-02-11)
Pyyyy-MM-dd1987-02-11
PPy-MMM-dd1987-feb-11
PPPy-MMMM-dd1987-februaro-11
PPPPEEEE, do 'de' MMMM ymerkredo, 11-a de februaro 1987
pHH:mm12:13
ppHH:mm:ss12:13:14
pppHH:mm:ss z12:13:14 GMT+0
ppppHo 'horo kaj' m:ss zzzz12-a horo kaj 13:14 GMT+00:00
Pp / PPpp日期 + 时间组合1987-02-11 12:13 / 1987-feb-11 12:13:14

快照还验证了历史日期(1453-05-29T23:59:59.999Z)在各模板下的输出,用于覆盖非当前年份的边界。注意PPPP全称日期模板中do(日序数)与MMMM(月份全称)之间以'de'(世界语"的")连接,产生dimanĉo, 11-a de januaro 1987这类地道表达;而pppp全称时间使用Ho 'horo kaj' m:ss模板,输出12-a horo kaj 13:14 GMT+00:00("12 时与 13:14")。

需要特别说明ppp/pppp/PPPppp/PPPPpppp的 parse 回环结果均为Errored:这些模板包含时区(z/zzzz),parse对含时区偏移的完整时间串的解析在快照测试中统一标记为 Errored(快照渲染器对解析异常的统一表示),这并非 eo 独有,而是p系列长模板与parse能力边界的既定事实。

formatDistance:基于"现在时刻"的相对时间

快照第二章假设"现在"为 2000-01-01 00:00(UTC),逐一验证未来与过去两个方向的相对时间输出,并分别展示includeSeconds: trueaddSuffix: true两个选项的影响。输出由 formatDistance/index.ts 中的模板表驱动,核心结构是"单数(one)/复数(other)"二元组,other中的{{count}}会被实际数值替换:

语义单数形式复数形式
lessThanXSecondsmalpli ol sekundomalpli ol {{count}} sekundoj
xSeconds1 sekundo{{count}} sekundoj
halfAMinuteduonminuto—(固定字符串)
aboutXHoursproksimume 1 horoproksimume {{count}} horoj
xDays1 tago{{count}} tagoj
aboutXMonthsproksimume 1 monatoproksimume {{count}} monatoj
aboutXYearsproksimume 1 jaroproksimume {{count}} jaroj
overXYearspli ol 1 jaropli ol {{count}} jaroj
almostXYearspreskaŭ 1 jaropreskaŭ {{count}} jaroj

快照中的代表性结果(未来方向):

输入日期默认结果includeSeconds: trueaddSuffix: true
2006-01-01proksimume 6 jarojproksimume 6 jarojpost proksimume 6 jaroj
2001-06-01pli ol 1 jaropli ol 1 jaropost pli ol 1 jaro
2000-01-01T00:45proksimume 1 horoproksimume 1 horopost proksimume 1 horo
2000-01-01T00:00:25malpli ol minutoduonminutopost malpli ol minuto
2000-01-01T00:00:00malpli ol minutomalpli ol 5 sekundojantaŭ malpli ol minuto

关键规则:未来时间使用post(之后),过去时间使用antaŭ(之前)。这一分支在 formatDistance 实现 中:

if (options?.addSuffix) { if (options?.comparison && options.comparison > 0) { return "post " + result; } else { return "antaŭ " + result; } }

includeSeconds: true会在分钟以内的时间上启用秒级粒度:duonminuto(半分钟)、malpli ol 10/20 sekundoj(不到 10/20 秒)、malpli ol 5 sekundoj(不到 5 秒,用于零差时刻)。xSeconds的快照覆盖同样出现在这一节中(如25 sekundoj)。

formatDistanceStrict:精确距离

formatDistance的"约/多于/少于"措辞不同,formatDistanceStrict输出精确数值,快照第三章节同样以 2000-01-01 00:00 为基准,并额外验证强制单位unit: "hour")下的换算:

输入日期默认结果addSuffix: true强制 hour 单位
2006-01-016 jarojpost 6 jaroj52608 horoj
2001-06-011 jaropost 1 jaro12408 horoj
2000-06-015 monatojpost 5 monatoj3648 horoj
2000-01-1514 tagojpost 14 tagoj336 horoj
2000-01-01T06:006 horojpost 6 horoj6 horoj
2000-01-01T00:4545 minutojpost 45 minutoj1 horo
2000-01-01T00:00:2525 sekundojpost 25 sekundoj0 horoj
2000-01-01T00:00:000 sekundojantaŭ 0 sekundoj0 horoj

过去方向(如 1999-12-31 23:59:55 →5 sekundoj、1999-06-01 →7 monatoj、1996-01-01 →4 jaroj)同样逐行验证,确保antaŭ前缀与强制单位换算在跨年、跨月边界下保持一致。强制hour单位时,小于 1 小时的时间差(如 45 分钟)按四舍五入得到1 horo,而00:15(15 分钟)则得到0 horoj——这是 strict 模式固定取整规则在快照中的直接体现。

formatRelative:相对日期短语

快照第四章节验证formatRelative(如"昨天/今天/明天/上周 X"),模板定义在 formatRelative/index.ts:

const formatRelativeLocale = { lastWeek: "'pasinta' eeee 'je' p", yesterday: "'hieraŭ je' p", today: "'hodiaŭ je' p", tomorrow: "'morgaŭ je' p", nextWeek: "eeee 'je' p", other: "P", };

以 2000-01-01(周六)为基准,快照结果:

输入日期结果
2000-01-102000-01-10(超出下周范围,回退P短日期)
2000-01-05merkredo je 00:00(下周 X:eeee 'je' p
2000-01-02morgaŭ je 00:00
2000-01-01hodiaŭ je 00:00
1999-12-31hieraŭ je 00:00
1999-12-27pasinta lundo je 00:00(上周 X:'pasinta' eeee 'je' p
1999-12-211999-12-21(超出上周范围,回退P

单引号包裹的字符串('pasinta''je''hodiaŭ je'等)是 date-fns token 中的字面量转义,表示原样输出;eeee展开为星期全称(lundo、merkredo 等),p展开为HH:mm短时间。注意"上周"使用pasinta(过去的)而"下周"直接用星期名,这一不对称正是世界语的自然表达习惯。

formatDuration:时长格式化

快照最后一个章节验证formatDuration,它把时长对象({years, months, weeks, days, hours, minutes, seconds})按 eo 词汇渲染为完整短语,单复数规则与formatDistance共享1 jaro / 2 jaroj这类词形变化:

输入结果输入结果
{"years":0}0 jaroj{"hours":0}0 horoj
{"years":1}1 jaro{"hours":1}1 horo
{"years":2}2 jaroj{"hours":2}2 horoj
{"months":0}0 monatoj{"minutes":0}0 minutoj
{"months":1}1 monato{"minutes":1}1 minuto
{"months":2}2 monatoj{"minutes":2}2 minutoj
{"weeks":0}0 semajnoj{"seconds":0}0 sekundoj
{"weeks":1}1 semajno{"seconds":1}1 sekundo
{"weeks":2}2 semajnoj{"seconds":2}2 sekundoj
{"days":0}0 tagoj
{"days":1}1 tago
{"days":2}2 tagoj

世界语的复数词尾规律(-o-oj-a-aj,以及tago→tagojsemajno→semajnoj)在这里完整呈现。

源码对照:快照背后的五块拼图

eo locale 的实现由五个模块组成,快照的每一列都能在源码中找到对应物:

  1. localize/index.tseraValuesquarterValuesmonthValuesdayValuesdayPeriodValues五个数据表 +ordinalNumber,通过buildLocalizeFn装配,快照中所有窄/缩写/全称输出均源自这里。季度缩写K1/K2正是quarterValues.abbreviated的字面值。
  2. match/index.ts:为每个语义提供matchPatterns(匹配)与parsePatterns(解析)正则。注意它对世界语特有字符做了多拼写兼容a(ŭ|ux|uh|u)g同时接受aŭgauxguhgaug(x/h 体系转写),(ĵ|jx|jh|j)a(ŭ|ux|uh|u)do同理——这解释了快照中aŭg等含变音符输出的可解析性来源。
  3. formatDistance/index.ts:17 个语义 token 的one/other模板与post/antaŭ后缀逻辑。
  4. formatLong/index.tsP/p系列四档日期、四档时间模板及{{date}} {{time}}组合模板。
  5. formatRelative/index.ts:五个相对位置模板 +P回退。

入口 index.ts 将上述模块聚合成eo: Locale对象,并声明code: "eo"weekStartsOn: 1firstWeekContainsDate: 4——后者正是Yo/wo/eo等本地周 token 的解析基准。

实用建议

  • 调试本地化输出:修改 eo 或任何 locale 的_lib/数据后,运行pnpm --filter date-fns locale-snapshots(脚本定义见 package.json)重新生成快照并 diff,即可一眼看出哪些 token 输出变化;CI 的test模式会保证快照与实现同步。
  • 警惕窄格式歧义MMMMM(J/F/M/A/S/O/N/D)在 parse 回环中不可唯一反解,涉及用户输入解析的业务应优先使用缩写(MMM)或全称(MMMM)token。
  • 柔性日时段注意回环边界BBBBB的下午时段(posttagmeze)parse 结果为Invalid Date,若需双向转换请使用BBBBB或普通a系列。
  • 时区模板不可回环ppp/pppp等含z/zzzz的模板输出无法被parse还原(快照中统一为Errored),这是 date-fns 对长时区文本解析的既定边界,并非 eo 缺陷。

综上,eo/snapshot.md 用七组数据表完整刻画了世界语 locale 在 date-fns 中的行为契约:序数-a后缀、a.t.m./p.t.m.时段标记、post/antaŭ相对前缀、ISO 风格周制,以及P/p系列的长格式模板。理解这份快照的读法,也就掌握了阅读 date-fns 全部 90+ locale 快照的通用方法。

【免费下载链接】date-fns⏳ Modern JavaScript date utility library ⌛️项目地址: https://gitcode.com/gh_mirrors/da/date-fns

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

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

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

立即咨询