isodoc-i18n 1.4.5 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.adoc +46 -1
- data/docs/extended-date-format.adoc +278 -0
- data/isodoc-i18n.gemspec +3 -1
- data/lib/isodoc/date.rb +5 -52
- data/lib/isodoc/extended_date.rb +227 -0
- data/lib/isodoc/i18n/version.rb +1 -1
- metadata +33 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7698b9379cd9b2bfea54973ec2cdaef465b92963e2e6e38715d20ef50edf58c1
|
|
4
|
+
data.tar.gz: c6370d69fcc7c5a5301b98582538853f29c6bd9455082b1b2e2e0b882e3ec0f3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7815dca37d2d415d53afe5a41260f650d8ff8f5b61a5d1fe200d0ad8de4fb008813f9e0737ceca81abf30c9dad15bd341c2e4db478787ff55d34a9901ef15b9b
|
|
7
|
+
data.tar.gz: 58f22f0df18829864fac021c3cb7e66c7d520c82eb92c385ecde375005341f27bb9f6546f8e4f4963f57826b05328bcb1cc4ba7fec11928a153002261d5b08a7
|
data/README.adoc
CHANGED
|
@@ -7,4 +7,49 @@ image:https://img.shields.io/github/issues-pr-raw/metanorma/isodoc-i18n.svg["Pul
|
|
|
7
7
|
image:https://img.shields.io/github/commits-since/metanorma/isodoc-i18n/latest.svg["Commits since latest",link="https://github.com/metanorma/isodoc-i18n/releases"]
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
Internationalisation for Metanorma rendering
|
|
10
|
+
Internationalisation for Metanorma rendering.
|
|
11
|
+
|
|
12
|
+
`isodoc-i18n` is the shared i18n/l10n base layer used across the
|
|
13
|
+
metanorma flavours. It provides:
|
|
14
|
+
|
|
15
|
+
* **YAML-driven locale labels** (`IsoDoc::I18n#labels`, `populate`) —
|
|
16
|
+
per-language label sets loaded from i18n YAML, with cross-file
|
|
17
|
+
references and Liquid template interpolation.
|
|
18
|
+
* **CLDR-backed l10n of punctuation and spacing** (`#l10n`) — CJK
|
|
19
|
+
contextual half-width/full-width punctuation, French spacing rules,
|
|
20
|
+
bidi wrapping for RTL scripts.
|
|
21
|
+
* **Grammatical inflection and ordinals** (`#inflect`, `#inflect_ordinal`)
|
|
22
|
+
— number/case/gender/person inflection driven by per-language YAML
|
|
23
|
+
inflection tables, plus CLDR `OrdinalRules` and `SpelloutRules`
|
|
24
|
+
formatting.
|
|
25
|
+
* **Liquid filter integration** (`IsoDoc::I18n::Liquid`) —
|
|
26
|
+
`inflect`, `ordinal_num`, `ordinal_word`, etc. registered globally
|
|
27
|
+
on `Liquid::Environment.default` for use in metanorma templates.
|
|
28
|
+
* **Boolean conjunction list rendering** (`#boolean_conj`) —
|
|
29
|
+
locale-correct "A, B, and C"-style joins.
|
|
30
|
+
* **Extended date formatting** (`IsoDoc::I18n#date`,
|
|
31
|
+
`IsoDoc::ExtendedDateFormatter`) — see below.
|
|
32
|
+
|
|
33
|
+
== Extended date formatting
|
|
34
|
+
|
|
35
|
+
`isodoc-i18n` ships an extended `strftime`-style date formatter,
|
|
36
|
+
`IsoDoc::ExtendedDateFormatter`, that adds POSIX-flavoured era and
|
|
37
|
+
alternative-numbering surfaces (Japanese era years, kanji spell-out,
|
|
38
|
+
Han digits, Roman months) on top of Ruby's `Date#strftime`. It is the
|
|
39
|
+
engine behind `IsoDoc::I18n#date` and metanorma's
|
|
40
|
+
`<date value="…" format="…"/>` element, and can also be used standalone:
|
|
41
|
+
|
|
42
|
+
[source,ruby]
|
|
43
|
+
----
|
|
44
|
+
require "isodoc-i18n"
|
|
45
|
+
|
|
46
|
+
f = IsoDoc::ExtendedDateFormatter.new(lang: "ja", script: "Jpan")
|
|
47
|
+
f.format("2024-09-30", "%EY年%-m月%-d日") # => "令和6年9月30日"
|
|
48
|
+
|
|
49
|
+
g = IsoDoc::ExtendedDateFormatter.new(lang: "en", script: "Latn")
|
|
50
|
+
g.format("2024-09-30", "%-d.%Om{roman}.%Y") # => "30.IX.2024"
|
|
51
|
+
----
|
|
52
|
+
|
|
53
|
+
Full token reference, calendar dispatch rules, and a comparison with
|
|
54
|
+
`twitter_cldr`, `japanese_calendar`, `ffi-icu`, and other adjacent
|
|
55
|
+
tooling: link:docs/extended-date-format.adoc[`docs/extended-date-format.adoc`].
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
= Extended date formatting in isodoc-i18n
|
|
2
|
+
|
|
3
|
+
== Scope
|
|
4
|
+
|
|
5
|
+
This document covers **only the extensions** introduced by
|
|
6
|
+
`isodoc-i18n` on top of Ruby's standard `Date#strftime`. Specifically:
|
|
7
|
+
|
|
8
|
+
- Era-year tokens (`%EY`, `%Ey`, `%EC`)
|
|
9
|
+
- Alternative-numbering tokens (`%Om`, `%Od`, `%OY`, `%Oy`)
|
|
10
|
+
- The legacy `%_`-as-literal-space alias kept for backwards compatibility
|
|
11
|
+
|
|
12
|
+
Everything else — the full set of `%Y`, `%m`, `%d`, `%H`, `%M`, `%S`,
|
|
13
|
+
`%F`, `%T`, `%j`, `%U`, `%W`, `%Z`, `%z`, etc. — is plain Ruby
|
|
14
|
+
`Date#strftime` and is documented at
|
|
15
|
+
https://docs.ruby-lang.org/en/master/strftime_formatting_rdoc.html.
|
|
16
|
+
Read that page first; this document is the *delta*.
|
|
17
|
+
|
|
18
|
+
The localised name directives (`%B`, `%b`, `%h`, `%A`, `%a`, `%P`,
|
|
19
|
+
`%p`, including their `%^B` etc. uppercase variants) are also part of
|
|
20
|
+
Ruby's standard strftime, but `isodoc-i18n` reroutes them through CLDR
|
|
21
|
+
locale data so the names come out in the configured `lang`/`script`
|
|
22
|
+
rather than in Ruby's hard-coded English. The rerouting is byte-identical
|
|
23
|
+
in surface — the *tokens* are unchanged from Ruby — so it is
|
|
24
|
+
documented in the *Localised names* section below as a pointer rather
|
|
25
|
+
than a re-specification.
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
== Background and dependencies
|
|
29
|
+
|
|
30
|
+
=== twitter_cldr
|
|
31
|
+
|
|
32
|
+
`twitter_cldr` is the Ruby port of the
|
|
33
|
+
https://cldr.unicode.org/[Unicode Common Locale Data Repository (CLDR)] — the canonical
|
|
34
|
+
multilingual data set for month names, day names, period markers
|
|
35
|
+
(am/pm), spell-out numbering rules, and so on. Anything in this
|
|
36
|
+
formatter that depends on a locale (the localised-name directives,
|
|
37
|
+
the `spellout` numbering system, the `hanidec`-friendly digit table)
|
|
38
|
+
is ultimately a query against CLDR through twitter_cldr. Repository
|
|
39
|
+
and API docs: https://github.com/twitter/twitter-cldr-rb.
|
|
40
|
+
|
|
41
|
+
=== japanese_calendar
|
|
42
|
+
|
|
43
|
+
`japanese_calendar` is a small Ruby gem that adds `Date#era_year` and
|
|
44
|
+
the `%JN`/`%Jy`/`%JH` strftime tokens for the Japanese era
|
|
45
|
+
(*gengō*) calendar. Used by the formatter for `cal=japanese`.
|
|
46
|
+
Repository: https://github.com/junmt/japanese_calendar.
|
|
47
|
+
|
|
48
|
+
=== POSIX `%E*` / `%O*` modifier convention
|
|
49
|
+
|
|
50
|
+
The token namespace `%E[YyC]` and `%O[mdYy]` is borrowed from POSIX
|
|
51
|
+
strftime, where `%E` introduces *alternative era representation* and
|
|
52
|
+
`%O` introduces *alternative numeric symbols* for a directive. POSIX
|
|
53
|
+
itself only defines the modifier prefixes; the actual era and
|
|
54
|
+
numbering data is locale-driven. The richest documented expansion is
|
|
55
|
+
GNU strftime, mirrored in Perl as
|
|
56
|
+
https://metacpan.org/dist/POSIX-strftime-GNU[`POSIX::strftime::GNU`]; we follow its intent in
|
|
57
|
+
spirit. Ruby's own `Date#strftime` accepts `%E`/`%O` but treats them
|
|
58
|
+
as no-ops (passing through to the unmodified directive) — claiming
|
|
59
|
+
this surface for actual era/numbering behaviour is the central
|
|
60
|
+
extension here.
|
|
61
|
+
|
|
62
|
+
=== "Era", concretely
|
|
63
|
+
|
|
64
|
+
An *era* in this context is a regnal or calendar epoch named by a
|
|
65
|
+
character or short label, with a year count that resets at the start
|
|
66
|
+
of each new epoch. The Japanese calendar is the canonical example
|
|
67
|
+
in metanorma's user base: 令和 (Reiwa, started 2019-05-01), 平成
|
|
68
|
+
(Heisei, 1989-2019), 昭和 (Shōwa, 1926-1989), and so on. Year 6 of
|
|
69
|
+
Reiwa (`令和6`) is calendar-year 2024. The era handler also
|
|
70
|
+
accommodates the Republic of China (Minguo) and Thai Buddhist
|
|
71
|
+
calendars conceptually, where the "era" is a single perpetual count
|
|
72
|
+
that doesn't reset (Minguo year = Gregorian − 1911; Buddhist year =
|
|
73
|
+
Gregorian + 543) — see *Calendar dispatch* below for the current
|
|
74
|
+
wiring status.
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
== Quick reference
|
|
78
|
+
|
|
79
|
+
[source,ruby]
|
|
80
|
+
----
|
|
81
|
+
require "isodoc-i18n"
|
|
82
|
+
|
|
83
|
+
f = IsoDoc::ExtendedDateFormatter.new(lang: "ja", script: "Jpan")
|
|
84
|
+
|
|
85
|
+
f.format("2024-09-30", "%EY年%-m月%-d日")
|
|
86
|
+
# => "令和6年9月30日"
|
|
87
|
+
|
|
88
|
+
f.format("2024-09-30", "%EY{spellout}年%Om{spellout}月%Od{spellout}日")
|
|
89
|
+
# => "令和六年九月三十日"
|
|
90
|
+
|
|
91
|
+
g = IsoDoc::ExtendedDateFormatter.new(lang: "en", script: "Latn")
|
|
92
|
+
|
|
93
|
+
g.format("2024-09-30", "%-d.%Om{roman}.%Y")
|
|
94
|
+
# => "30.IX.2024"
|
|
95
|
+
|
|
96
|
+
g.format("2024-09-30", "%-d %B %Y")
|
|
97
|
+
# => "30 September 2024"
|
|
98
|
+
----
|
|
99
|
+
|
|
100
|
+
`format` accepts an ISO-8601 string, a `Date`, a `DateTime`, or a
|
|
101
|
+
`Time` as the first argument.
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
== Token reference
|
|
105
|
+
|
|
106
|
+
=== Era year — `%EY`, `%Ey`, `%EC`
|
|
107
|
+
|
|
108
|
+
[cols="1,3"]
|
|
109
|
+
|===
|
|
110
|
+
| Token | Meaning
|
|
111
|
+
|
|
112
|
+
| `%EY` | Era name + era year (e.g. `令和6` for 2024-09-30 in `cal=japanese`)
|
|
113
|
+
| `%Ey` | Era year only (e.g. `6`)
|
|
114
|
+
| `%EC` | Era name only (e.g. `令和`)
|
|
115
|
+
|===
|
|
116
|
+
|
|
117
|
+
The argument block selects the numbering system used to render the
|
|
118
|
+
year, and (optionally) the calendar:
|
|
119
|
+
|
|
120
|
+
[source]
|
|
121
|
+
----
|
|
122
|
+
%EY{numeric} # 令和6 (default)
|
|
123
|
+
%EY{spellout} # 令和六
|
|
124
|
+
%EY{cal=japanese} # 令和6
|
|
125
|
+
%EY{spellout, cal=japanese}
|
|
126
|
+
----
|
|
127
|
+
|
|
128
|
+
The default calendar is `japanese` when `lang: "ja"`, otherwise
|
|
129
|
+
`gregorian`. Under `cal=gregorian`, `%EY` collapses to the four-digit
|
|
130
|
+
year, `%Ey` to the two-digit year, and `%EC` to an empty string —
|
|
131
|
+
the era surface degrades cleanly for locales without an era
|
|
132
|
+
convention.
|
|
133
|
+
|
|
134
|
+
For pre-Meiji dates (before 1868), `japanese_calendar.era_year`
|
|
135
|
+
raises; the formatter catches this and falls back to the plain
|
|
136
|
+
Gregorian year.
|
|
137
|
+
|
|
138
|
+
=== Alternative numbering — `%Om`, `%Od`, `%OY`, `%Oy`
|
|
139
|
+
|
|
140
|
+
[cols="1,3"]
|
|
141
|
+
|===
|
|
142
|
+
| Token | Meaning
|
|
143
|
+
|
|
144
|
+
| `%Om` | Month rendered in an alternative numbering system
|
|
145
|
+
| `%Od` | Day rendered in an alternative numbering system
|
|
146
|
+
| `%OY` | Four-digit year in an alternative numbering system
|
|
147
|
+
| `%Oy` | Two-digit year in an alternative numbering system
|
|
148
|
+
|===
|
|
149
|
+
|
|
150
|
+
The argument block names the numbering system. Supported values:
|
|
151
|
+
|
|
152
|
+
[cols="1,2,3"]
|
|
153
|
+
|===
|
|
154
|
+
| Keyword | Behaviour | Example for 30
|
|
155
|
+
|
|
156
|
+
| `numeric` (default) | Latin digits | `30`
|
|
157
|
+
| `latn` | Alias for `numeric` | `30`
|
|
158
|
+
| `spellout` | CLDR `spellout-cardinal` rules in `lang` | `三十` (ja)
|
|
159
|
+
| `hanidec` | Han decimal digits, digit-for-digit | `三〇`
|
|
160
|
+
| `roman` | Roman numerals, uppercase | `XXX`
|
|
161
|
+
| `roman-lower` | Roman numerals, lowercase | `xxx`
|
|
162
|
+
|===
|
|
163
|
+
|
|
164
|
+
`spellout` and `hanidec` are visually similar for single-digit
|
|
165
|
+
numbers but diverge above 9: `spellout` produces positional kanji
|
|
166
|
+
(`三十` for 30) while `hanidec` is digit-for-digit substitution
|
|
167
|
+
(`三〇`). Pick based on which output the document style requires.
|
|
168
|
+
|
|
169
|
+
`roman` and `roman-lower` ignore `lang` — the digits are
|
|
170
|
+
language-independent. They are intended for Continental European
|
|
171
|
+
bibliographic styles such as `30.IX.2024` and standards documents
|
|
172
|
+
that quote dates with Roman months as a typographic convention.
|
|
173
|
+
|
|
174
|
+
=== Localised names — `%B`, `%b`, `%h`, `%A`, `%a`, `%P`, `%p`
|
|
175
|
+
|
|
176
|
+
These are standard Ruby strftime tokens — see the canonical Ruby
|
|
177
|
+
reference at
|
|
178
|
+
https://docs.ruby-lang.org/en/master/strftime_formatting_rdoc.html.
|
|
179
|
+
`isodoc-i18n` does not change their syntax; it changes their
|
|
180
|
+
*output* by reading the localised month/day/period names from CLDR
|
|
181
|
+
via `twitter_cldr` for the configured `lang`/`script`. The CLDR data
|
|
182
|
+
keys consulted are
|
|
183
|
+
`calendar_data[:months][:format][:wide|:abbreviated]`,
|
|
184
|
+
`calendar_data[:days][:format][:wide|:abbreviated]`, and
|
|
185
|
+
`periods[:am|:pm]`; see the twitter_cldr README at
|
|
186
|
+
https://github.com/twitter/twitter-cldr-rb#calendars-amp-dates for
|
|
187
|
+
the underlying API.
|
|
188
|
+
|
|
189
|
+
`%^B`, `%^A`, etc. uppercase the localised result. Behaviour is
|
|
190
|
+
byte-identical to the previous `IsoDoc::I18n#date` implementation,
|
|
191
|
+
so existing format strings such as `%-d %B %Y` continue to produce
|
|
192
|
+
the same output.
|
|
193
|
+
|
|
194
|
+
=== Legacy — `%_`
|
|
195
|
+
|
|
196
|
+
`%_` is treated as a literal space, preserving the legacy
|
|
197
|
+
`IsoDoc::I18n#date` convention. Note that this overrides Ruby's
|
|
198
|
+
POSIX `%_` flag (which means "space-pad the next directive"); kept
|
|
199
|
+
for backwards compatibility with existing metanorma format strings.
|
|
200
|
+
|
|
201
|
+
=== Calendar dispatch (extension surface)
|
|
202
|
+
|
|
203
|
+
The `cal=` argument is a CLDR calendar identifier. Two calendars are
|
|
204
|
+
wired up today:
|
|
205
|
+
|
|
206
|
+
[cols="1,3"]
|
|
207
|
+
|===
|
|
208
|
+
| Calendar | Status
|
|
209
|
+
|
|
210
|
+
| `gregorian` | Default for non-Japanese locales. Era tokens degrade as described above.
|
|
211
|
+
| `japanese` | Default for `lang: "ja"`. Backed by `japanese_calendar`.
|
|
212
|
+
|===
|
|
213
|
+
|
|
214
|
+
The following identifiers are reserved as extension points and raise
|
|
215
|
+
`NotImplementedError` today, with a clear message naming the
|
|
216
|
+
supported set:
|
|
217
|
+
|
|
218
|
+
`roc`, `buddhist`, `persian`, `islamic`, `indian`, `hebrew`.
|
|
219
|
+
|
|
220
|
+
These map to CLDR data already shipped by `twitter_cldr`. They are
|
|
221
|
+
documented as future-friendly entry points so user code can be
|
|
222
|
+
written against the eventual API; wiring is a configuration matter,
|
|
223
|
+
not a research project, and will be added on demand.
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
== Comparison of alternatives
|
|
227
|
+
|
|
228
|
+
When `isodoc-i18n` doesn't fit, these are the gems and standards that
|
|
229
|
+
cover adjacent surface area:
|
|
230
|
+
|
|
231
|
+
[cols="2,3,3"]
|
|
232
|
+
|===
|
|
233
|
+
| Resource | Scope | Limitation relative to isodoc-i18n
|
|
234
|
+
|
|
235
|
+
| https://github.com/twitter/twitter-cldr-rb[`twitter_cldr`]
|
|
236
|
+
| CLDR locale data; CLDR-skeleton formatting (`to_additional_s`); spell-out numbering (`to_rbnf_s`)
|
|
237
|
+
| No POSIX `%E*`/`%O*` surface; alternative numbering systems not exposed cleanly per date component
|
|
238
|
+
|
|
239
|
+
| https://github.com/junmt/japanese_calendar[`japanese_calendar`]
|
|
240
|
+
| `Date#era_year` plus `%JN`/`%Jy`/`%JH` strftime tokens
|
|
241
|
+
| Japan-only; no alternative numbering for the regnal year (we layer twitter_cldr on top)
|
|
242
|
+
|
|
243
|
+
| https://github.com/sho-h/era_ja[`era_ja`]
|
|
244
|
+
| Like `japanese_calendar`, plus `%K` for the "元年" first-year convention
|
|
245
|
+
| Japan-only; standalone — would duplicate `japanese_calendar`'s surface
|
|
246
|
+
|
|
247
|
+
| https://github.com/ffi/ffi-icu[`ffi-icu`]
|
|
248
|
+
| Full ICU bindings: native era, numbering systems, locale-aware everything
|
|
249
|
+
| Requires `libicu` system dependency; steep API; heavyweight if you only need date formatting
|
|
250
|
+
|
|
251
|
+
| https://github.com/ruby-i18n/ruby-cldr[`ruby-cldr`]
|
|
252
|
+
| CLDR data export
|
|
253
|
+
| Data only, no formatting API
|
|
254
|
+
|
|
255
|
+
| https://metacpan.org/dist/POSIX-strftime-GNU[`POSIX::strftime::GNU` (Perl)]
|
|
256
|
+
| Reference implementation of GNU strftime `%E*`/`%O*` modifiers
|
|
257
|
+
| Perl only; no Ruby port — used here as a design reference
|
|
258
|
+
|
|
259
|
+
| https://rubygems.org/gems/roman-numerals[`roman-numerals`]
|
|
260
|
+
| Roman numeral conversion
|
|
261
|
+
| No date-formatting surface; isodoc-i18n wraps it for `%Om{roman}`
|
|
262
|
+
|
|
263
|
+
| https://shopify.github.io/liquid/filters/date[Liquid `date` filter]
|
|
264
|
+
| Vanilla Ruby strftime in Liquid templates
|
|
265
|
+
| No locale, no era, no Roman; a Liquid filter built on this formatter is the wrapper that adds those
|
|
266
|
+
|
|
267
|
+
| https://liquidjs.com/filters/date.html[LiquidJS `date_to_xmlschema` / `date`]
|
|
268
|
+
| Liquid for JavaScript runtimes; adds `%q` ordinal
|
|
269
|
+
| No era, no alternative numbering; not relevant to a Ruby pipeline but listed for completeness
|
|
270
|
+
|
|
271
|
+
| https://cldr.unicode.org/[CLDR]
|
|
272
|
+
| The data source `twitter_cldr` re-exposes
|
|
273
|
+
| Data spec, not a formatting API
|
|
274
|
+
|
|
275
|
+
| https://unicode-org.github.io/icu/[ICU]
|
|
276
|
+
| C/C++ implementation behind `ffi-icu`
|
|
277
|
+
| C library, requires bindings
|
|
278
|
+
|===
|
data/isodoc-i18n.gemspec
CHANGED
|
@@ -20,12 +20,14 @@ Gem::Specification.new do |spec|
|
|
|
20
20
|
f.match(%r{^(test|spec|features|bin|.github)/}) \
|
|
21
21
|
|| f.match(%r{Rakefile|bin/rspec})
|
|
22
22
|
end
|
|
23
|
-
spec.required_ruby_version = Gem::Requirement.new(">= 2.
|
|
23
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 3.2.0")
|
|
24
24
|
|
|
25
25
|
spec.add_dependency "base64"
|
|
26
26
|
spec.add_dependency "htmlentities", "~> 4.3.4"
|
|
27
|
+
spec.add_dependency "japanese_calendar"
|
|
27
28
|
spec.add_dependency "liquid", "~> 5"
|
|
28
29
|
spec.add_dependency "metanorma-utils", ">= 1.7.0"
|
|
30
|
+
spec.add_dependency "roman-numerals"
|
|
29
31
|
spec.add_dependency "twitter_cldr"
|
|
30
32
|
|
|
31
33
|
spec.add_development_dependency "canon"
|
data/lib/isodoc/date.rb
CHANGED
|
@@ -1,60 +1,13 @@
|
|
|
1
1
|
require "date"
|
|
2
|
+
require_relative "extended_date"
|
|
2
3
|
|
|
3
4
|
module IsoDoc
|
|
4
5
|
class I18n
|
|
5
6
|
def date(value, format)
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def convert_date_format(fmt)
|
|
11
|
-
fmt.gsub(/%_/, " ")
|
|
12
|
-
.gsub(/%(\^?)([BbhPpAa])/, "%\u200c\\1\\2<%\\2>")
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def date_i18n(val)
|
|
16
|
-
day_i18n(month_i18n(am_pm_i18n(val)))
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def am_pm_i18n(val)
|
|
20
|
-
val.gsub(/%\u200cP<am>/, @cal.periods[:am].downcase)
|
|
21
|
-
.gsub(/%\u200cP<pm>/, @cal.periods[:pm].downcase)
|
|
22
|
-
.gsub(/%\u200cp<AM>/, @cal.periods[:am].upcase)
|
|
23
|
-
.gsub(/%\u200cp<PM>/, @cal.periods[:pm].upcase)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def month_i18n(val)
|
|
27
|
-
{ B: :wide, b: :abbreviated, h: :abbreviated }.each do |f, t|
|
|
28
|
-
@cal_en.calendar_data[:months][:format][t].each do |k, v|
|
|
29
|
-
m = @cal.calendar_data[:months][:format][t][k]
|
|
30
|
-
val.gsub!(/%\u200c#{f}<#{v}>/, m)
|
|
31
|
-
val.gsub!(/%\u200c\^#{f}<#{v}>/, m.upcase)
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
val
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def day_i18n(val)
|
|
38
|
-
{ A: :wide, a: :abbreviated }.each do |f, t|
|
|
39
|
-
@cal_en.calendar_data[:days][:format][t].each do |k, v|
|
|
40
|
-
m = @cal.calendar_data[:days][:format][t][k]
|
|
41
|
-
val.gsub!(/%\u200c#{f}<#{v}>/, m)
|
|
42
|
-
val.gsub!(/%\u200c\^#{f}<#{v}>/, m.upcase)
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
val
|
|
7
|
+
ExtendedDateFormatter.new(
|
|
8
|
+
lang: @lang, script: @script,
|
|
9
|
+
calendar: @cal, calendar_en: @cal_en
|
|
10
|
+
).format(value, format)
|
|
46
11
|
end
|
|
47
12
|
end
|
|
48
13
|
end
|
|
49
|
-
|
|
50
|
-
# %B - The full month name (``January'')
|
|
51
|
-
# %^B uppercased (``JANUARY'')
|
|
52
|
-
# %b - The abbreviated month name (``Jan'')
|
|
53
|
-
# %^b uppercased (``JAN'')
|
|
54
|
-
# %h - Equivalent to %b
|
|
55
|
-
# %P - Meridian indicator, lowercase (``am'' or ``pm'')
|
|
56
|
-
# %p - Meridian indicator, uppercase (``AM'' or ``PM'')
|
|
57
|
-
# %A - The full weekday name (``Sunday'')
|
|
58
|
-
# %^A uppercased (``SUNDAY'')
|
|
59
|
-
# %a - The abbreviated name (``Sun'')
|
|
60
|
-
# %^a uppercased (``SUN'')
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
require "date"
|
|
2
|
+
require "twitter_cldr"
|
|
3
|
+
|
|
4
|
+
module IsoDoc
|
|
5
|
+
# Extended strftime-style date formatter on top of Ruby's +Date#strftime+.
|
|
6
|
+
#
|
|
7
|
+
# Adds three POSIX-flavoured surfaces:
|
|
8
|
+
#
|
|
9
|
+
# %E[YyC](?:\{ARGS\})? - era year (calendar-aware)
|
|
10
|
+
# %O[mdYy](?:\{ARGS\})? - alternative numbering for date components
|
|
11
|
+
# %_ - legacy alias for a literal space (kept for
|
|
12
|
+
# backwards compatibility with IsoDoc::I18n#date)
|
|
13
|
+
#
|
|
14
|
+
# The localised name directives (%B, %b, %h, %A, %a, %P, %p) are also
|
|
15
|
+
# routed through the formatter so they pick up CLDR locale data without
|
|
16
|
+
# the previous ZWNJ-marker hack in IsoDoc::I18n.
|
|
17
|
+
#
|
|
18
|
+
# ARGS is a comma-separated list of either a positional numbering-system
|
|
19
|
+
# name (numeric, spellout, hanidec, roman, roman-lower) or +key=value+
|
|
20
|
+
# pairs. The only key currently honoured is +cal=+ (japanese|gregorian);
|
|
21
|
+
# other CLDR calendar identifiers (roc, buddhist, persian, islamic,
|
|
22
|
+
# indian, hebrew) are reserved as documented extension points and raise
|
|
23
|
+
# NotImplementedError today.
|
|
24
|
+
class ExtendedDateFormatter
|
|
25
|
+
TOKEN_RX = /
|
|
26
|
+
%_ |
|
|
27
|
+
%\^?[BbhPpAa] |
|
|
28
|
+
%E[YyC](?:\{[^}]*\})? |
|
|
29
|
+
%O[mdYy](?:\{[^}]*\})?
|
|
30
|
+
/x.freeze
|
|
31
|
+
|
|
32
|
+
DAY_KEYS = %i[sun mon tue wed thu fri sat].freeze
|
|
33
|
+
HANIDEC_FROM = "0123456789".freeze
|
|
34
|
+
HANIDEC_TO = "〇一二三四五六七八九".freeze
|
|
35
|
+
|
|
36
|
+
SUPPORTED_CALENDARS = %i[gregorian japanese].freeze
|
|
37
|
+
|
|
38
|
+
def self.format(value, fmt, **opts)
|
|
39
|
+
new(**opts).format(value, fmt)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
attr_reader :lang, :script
|
|
43
|
+
|
|
44
|
+
def initialize(lang:, script: nil, calendar: nil, calendar_en: nil)
|
|
45
|
+
@lang = lang.to_s
|
|
46
|
+
@script = script
|
|
47
|
+
@cal = calendar || twitter_cldr_calendar
|
|
48
|
+
@cal_en = calendar_en || TwitterCldr::Shared::Calendar.new(:en)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def format(value, fmt)
|
|
52
|
+
time = coerce(value)
|
|
53
|
+
tokenise(fmt).map { |kind, payload| render(time, kind, payload) }.join
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def coerce(value)
|
|
59
|
+
case value
|
|
60
|
+
when DateTime, Time then value
|
|
61
|
+
when Date then value.to_datetime
|
|
62
|
+
else DateTime.iso8601(value.to_s)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def tokenise(fmt)
|
|
67
|
+
out = []
|
|
68
|
+
last = 0
|
|
69
|
+
fmt.to_enum(:scan, TOKEN_RX).each do
|
|
70
|
+
m = Regexp.last_match
|
|
71
|
+
out << [:strftime, fmt[last...m.begin(0)]] if m.begin(0) > last
|
|
72
|
+
out << [:token, m[0]]
|
|
73
|
+
last = m.end(0)
|
|
74
|
+
end
|
|
75
|
+
out << [:strftime, fmt[last..-1]] if last < fmt.length
|
|
76
|
+
out
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def render(time, kind, payload)
|
|
80
|
+
case kind
|
|
81
|
+
when :strftime then payload.empty? ? "" : time.strftime(payload)
|
|
82
|
+
when :token then render_token(time, payload)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def render_token(time, tok)
|
|
87
|
+
return " " if tok == "%_"
|
|
88
|
+
|
|
89
|
+
case tok
|
|
90
|
+
when /\A%(\^?)([BbhPpAa])\z/
|
|
91
|
+
render_localised_name(time, Regexp.last_match(1) == "^",
|
|
92
|
+
Regexp.last_match(2))
|
|
93
|
+
when /\A%E([YyC])(?:\{([^}]*)\})?\z/
|
|
94
|
+
render_era(time, Regexp.last_match(1),
|
|
95
|
+
parse_args(Regexp.last_match(2)))
|
|
96
|
+
when /\A%O([mdYy])(?:\{([^}]*)\})?\z/
|
|
97
|
+
render_alt_num(time, Regexp.last_match(1),
|
|
98
|
+
parse_args(Regexp.last_match(2)))
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def render_localised_name(time, upcase, letter)
|
|
103
|
+
day = DAY_KEYS[time.wday]
|
|
104
|
+
raw = case letter
|
|
105
|
+
when "B" then @cal.calendar_data[:months][:format][:wide][time.month]
|
|
106
|
+
when "b", "h"
|
|
107
|
+
@cal.calendar_data[:months][:format][:abbreviated][time.month]
|
|
108
|
+
when "A" then @cal.calendar_data[:days][:format][:wide][day]
|
|
109
|
+
when "a" then @cal.calendar_data[:days][:format][:abbreviated][day]
|
|
110
|
+
when "P" then @cal.periods[am_pm(time)].downcase
|
|
111
|
+
when "p" then @cal.periods[am_pm(time)].upcase
|
|
112
|
+
end
|
|
113
|
+
upcase ? raw.upcase : raw
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def am_pm(time)
|
|
117
|
+
time.respond_to?(:hour) && time.hour >= 12 ? :pm : :am
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def render_era(time, letter, args)
|
|
121
|
+
cal = (args[:cal] || default_calendar).to_sym
|
|
122
|
+
case cal
|
|
123
|
+
when :japanese then render_japanese_era(time, letter, args)
|
|
124
|
+
when :gregorian then render_gregorian_era(time, letter, args)
|
|
125
|
+
else
|
|
126
|
+
raise NotImplementedError,
|
|
127
|
+
"ExtendedDateFormatter: calendar #{cal.inspect} is a " \
|
|
128
|
+
"documented extension point but is not yet wired up. " \
|
|
129
|
+
"Supported: #{SUPPORTED_CALENDARS.inspect}."
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def render_japanese_era(time, letter, args)
|
|
134
|
+
require "japanese_calendar"
|
|
135
|
+
d = time.is_a?(Date) ? time : Date.new(time.year, time.month, time.day)
|
|
136
|
+
year = format_number(d.era_year, args)
|
|
137
|
+
case letter
|
|
138
|
+
when "Y" then "#{d.strftime('%JN')}#{year}"
|
|
139
|
+
when "y" then year
|
|
140
|
+
when "C" then d.strftime("%JN")
|
|
141
|
+
end
|
|
142
|
+
rescue StandardError
|
|
143
|
+
case letter
|
|
144
|
+
when "Y", "y" then format_number(time.year, args)
|
|
145
|
+
when "C" then ""
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def render_gregorian_era(time, letter, args)
|
|
150
|
+
case letter
|
|
151
|
+
when "Y" then format_number(time.year, args)
|
|
152
|
+
when "y" then format_number(time.year % 100, args)
|
|
153
|
+
when "C" then ""
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def render_alt_num(time, letter, args)
|
|
158
|
+
n = case letter
|
|
159
|
+
when "m" then time.month
|
|
160
|
+
when "d" then time.day
|
|
161
|
+
when "Y" then time.year
|
|
162
|
+
when "y" then time.year % 100
|
|
163
|
+
end
|
|
164
|
+
format_number(n, args)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def format_number(num, args)
|
|
168
|
+
sys = args[:_positional] || args[:numbering] || "numeric"
|
|
169
|
+
case sys
|
|
170
|
+
when "numeric", "latn" then num.to_s
|
|
171
|
+
when "spellout" then spellout(num)
|
|
172
|
+
when "hanidec" then num.to_s.tr(HANIDEC_FROM, HANIDEC_TO)
|
|
173
|
+
when "roman" then roman(num)
|
|
174
|
+
when "roman-lower" then roman(num).downcase
|
|
175
|
+
else
|
|
176
|
+
raise ArgumentError,
|
|
177
|
+
"ExtendedDateFormatter: numbering system #{sys.inspect} not " \
|
|
178
|
+
"supported. Use one of: numeric, spellout, hanidec, roman, " \
|
|
179
|
+
"roman-lower."
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def spellout(num)
|
|
184
|
+
num.to_i.localize(twitter_cldr_lang)
|
|
185
|
+
.to_rbnf_s("SpelloutRules", "spellout-cardinal")
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def roman(num)
|
|
189
|
+
require "roman-numerals"
|
|
190
|
+
RomanNumerals.to_roman(num.to_i)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def parse_args(str)
|
|
194
|
+
out = {}
|
|
195
|
+
return out if str.nil? || str.empty?
|
|
196
|
+
|
|
197
|
+
str.split(",").each do |arg|
|
|
198
|
+
arg = arg.strip
|
|
199
|
+
if arg.include?("=")
|
|
200
|
+
k, v = arg.split("=", 2)
|
|
201
|
+
out[k.strip.to_sym] = v.strip
|
|
202
|
+
else
|
|
203
|
+
out[:_positional] ||= arg
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
out
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def default_calendar
|
|
210
|
+
@lang == "ja" ? :japanese : :gregorian
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def twitter_cldr_lang
|
|
214
|
+
case [@lang, @script]
|
|
215
|
+
when ["zh", "Hans"] then :"zh-cn"
|
|
216
|
+
when ["zh", "Hant"] then :"zh-tw"
|
|
217
|
+
else @lang.to_sym
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def twitter_cldr_calendar
|
|
222
|
+
TwitterCldr::Shared::Calendar.new(twitter_cldr_lang)
|
|
223
|
+
rescue StandardError
|
|
224
|
+
TwitterCldr::Shared::Calendar.new(:en)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
data/lib/isodoc/i18n/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: isodoc-i18n
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-05-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -38,6 +38,20 @@ dependencies:
|
|
|
38
38
|
- - "~>"
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
40
|
version: 4.3.4
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: japanese_calendar
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
41
55
|
- !ruby/object:Gem::Dependency
|
|
42
56
|
name: liquid
|
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -66,6 +80,20 @@ dependencies:
|
|
|
66
80
|
- - ">="
|
|
67
81
|
- !ruby/object:Gem::Version
|
|
68
82
|
version: 1.7.0
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: roman-numerals
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0'
|
|
90
|
+
type: :runtime
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0'
|
|
69
97
|
- !ruby/object:Gem::Dependency
|
|
70
98
|
name: twitter_cldr
|
|
71
99
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -263,10 +291,12 @@ files:
|
|
|
263
291
|
- Gemfile
|
|
264
292
|
- LICENSE
|
|
265
293
|
- README.adoc
|
|
294
|
+
- docs/extended-date-format.adoc
|
|
266
295
|
- isodoc-i18n.gemspec
|
|
267
296
|
- lib/isodoc-i18n.rb
|
|
268
297
|
- lib/isodoc-yaml/i18n-en.yaml
|
|
269
298
|
- lib/isodoc/date.rb
|
|
299
|
+
- lib/isodoc/extended_date.rb
|
|
270
300
|
- lib/isodoc/i18n-yaml.rb
|
|
271
301
|
- lib/isodoc/i18n.rb
|
|
272
302
|
- lib/isodoc/i18n/version.rb
|
|
@@ -286,7 +316,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
286
316
|
requirements:
|
|
287
317
|
- - ">="
|
|
288
318
|
- !ruby/object:Gem::Version
|
|
289
|
-
version: 2.
|
|
319
|
+
version: 3.2.0
|
|
290
320
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
291
321
|
requirements:
|
|
292
322
|
- - ">="
|