cronli5 0.3.1 → 0.7.2
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.
- package/CHANGELOG.md +141 -0
- package/README.md +41 -8
- package/cronli5.min.js +2 -2
- package/dist/cronli5.cjs +13 -6
- package/dist/cronli5.js +13 -6
- package/dist/lang/de.cjs +13 -6
- package/dist/lang/de.js +13 -6
- package/dist/lang/en.cjs +13 -6
- package/dist/lang/en.js +13 -6
- package/dist/lang/es.cjs +13 -6
- package/dist/lang/es.js +13 -6
- package/dist/lang/fi.cjs +13 -6
- package/dist/lang/fi.js +13 -6
- package/dist/lang/fr.cjs +1210 -0
- package/dist/lang/fr.js +1186 -0
- package/dist/lang/pt.cjs +1591 -0
- package/dist/lang/pt.js +1567 -0
- package/dist/lang/zh.cjs +73 -17
- package/dist/lang/zh.js +73 -17
- package/package.json +18 -7
- package/src/core/cadence.ts +25 -12
- package/src/lang/de/index.ts +2 -2
- package/src/lang/en/index.ts +2 -2
- package/src/lang/es/index.ts +2 -2
- package/src/lang/fi/index.ts +2 -2
- package/src/lang/fr/dialects.ts +49 -0
- package/src/lang/fr/index.ts +2115 -0
- package/src/lang/fr/notes.md +280 -0
- package/src/lang/fr/status.json +8 -0
- package/src/lang/pt/dialects.ts +56 -0
- package/src/lang/pt/index.ts +2803 -0
- package/src/lang/pt/notes.md +199 -0
- package/src/lang/pt/status.json +8 -0
- package/src/lang/zh/index.ts +106 -17
- package/src/lang/zh/notes.md +16 -4
- package/src/lang/zh/status.json +10 -1
- package/types/core/cadence.d.ts +1 -0
- package/types/lang/fr/dialects.d.ts +11 -0
- package/types/lang/fr/index.d.ts +4 -0
- package/types/lang/pt/dialects.d.ts +13 -0
- package/types/lang/pt/index.d.ts +4 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Português (pt, target pt-BR) — Language Notes
|
|
2
|
+
|
|
3
|
+
**donor: es.** Derived by sibling-derivation (tooling/docs/language-pipeline.md):
|
|
4
|
+
the Spanish module supplies the structure, plan override, OR-frame, predicates,
|
|
5
|
+
re-strategies, and dialect mechanism; this doc records only where **pt-BR
|
|
6
|
+
diverges** from that donor. The shipped table today is **pt-BR**; pt-PT is a
|
|
7
|
+
future dialect axis (below). The corpus translation (Stage 2) and renderer port
|
|
8
|
+
(Stage 4) both follow this contract. The donor's own contract is
|
|
9
|
+
[`../es/notes.md`](../es/notes.md).
|
|
10
|
+
|
|
11
|
+
## Anchors
|
|
12
|
+
|
|
13
|
+
Brazilian norm (VOLP / Academia Brasileira de Letras, plus cronstrue `pt_BR`):
|
|
14
|
+
lowercase month and weekday names, **24-hour zero-padded clock by default**
|
|
15
|
+
("às 09:30", "às 17:00"; `{ampm: true}` opts into the 12-hour clock), day
|
|
16
|
+
periods on the 12-hour clock (madrugada 1–5, manhã 6–11, tarde 12–18, noite
|
|
17
|
+
19–24 — see the note below; this is the one boundary that may differ from es),
|
|
18
|
+
"meio-dia" / "meia-noite" for exact 12:00 / 0:00 (12-hour clock), colon time
|
|
19
|
+
separator.
|
|
20
|
+
|
|
21
|
+
**Clock — decided: "às 09:00" (24h zero-padded), not the colloquial "às 9h".**
|
|
22
|
+
Rationale: parallels the es donor and cronstrue `pt_BR`'s reference rendering,
|
|
23
|
+
and keeps the corpus comparable field-by-field with es; the "9h"/"9h30" form is
|
|
24
|
+
genuinely common in pt-BR casual writing but is a *separate register*, deferred
|
|
25
|
+
to a future custom/dialect style exactly as es kept `hSuffix` opt-in.
|
|
26
|
+
|
|
27
|
+
**Article agreement (the a+a contraction):** the preposition *a* + the feminine
|
|
28
|
+
article contracts — *a + a hora 1* → **à 1h / à 01:00** (grave accent, singular),
|
|
29
|
+
*a + as horas* → **às** otherwise. This mirrors es's singular-article hold for
|
|
30
|
+
one o'clock ("a la 1") but in pt the contraction is *written with the accent*:
|
|
31
|
+
"à 01:00" at hour 1, "às 09:00" at every other hour, on **both** clocks. Hours
|
|
32
|
+
zero-pad to two digits on the 24-hour clock ("às 09:00"); the 12-hour clock
|
|
33
|
+
leaves the hour unpadded ("às 9 da manhã").
|
|
34
|
+
|
|
35
|
+
## Day periods (12-hour)
|
|
36
|
+
|
|
37
|
+
"da madrugada / da manhã / da tarde / da noite" (contraction *de + a* = *da*).
|
|
38
|
+
**Decided (panel-confirmed): madrugada 1–5, manhã 6–11, tarde 12–18, noite
|
|
39
|
+
19–24.** es uses tarde 12–19 / noche 20–24, but pt-BR puts *noite* earlier;
|
|
40
|
+
the blind pt-BR panel unanimously affirmed the 19h boundary (broadcast/weather
|
|
41
|
+
register and the "jornal da noite" cultural anchor place noite firmly at 19h —
|
|
42
|
+
tighter than the loose popular sense that some extend to 18h, and the better
|
|
43
|
+
choice for an unambiguous description). 18h reads *da tarde*, 19h+ *da noite*.
|
|
44
|
+
"meio-dia" / "meia-noite" for exact 12:00 / 0:00.
|
|
45
|
+
|
|
46
|
+
## Weekday recurrence
|
|
47
|
+
|
|
48
|
+
es uses the plural article "los lunes" = every Monday. **Decided (panel-
|
|
49
|
+
confirmed): keep *-feira* throughout** — the full forms are the standard pt-BR
|
|
50
|
+
written/spoken register and dropping *-feira* is too informal for an unambiguous
|
|
51
|
+
description. The *-feira* element attaches to weekdays Mon–Fri (segunda-feira …
|
|
52
|
+
sexta-feira); sábado and domingo have none. The resolved sub-rules:
|
|
53
|
+
|
|
54
|
+
- **Single weekday recurrence + a clock time → "toda segunda-feira às 9 da
|
|
55
|
+
manhã".** The plural-article recurrence "às segundas-feiras" before an "às …"
|
|
56
|
+
time clashed aurally (the double-"às"); the panel's fix is the singular
|
|
57
|
+
"toda X" head, which reads naturally and keeps the meaning. This applies
|
|
58
|
+
wherever a single weekday leads a clause that a clock time follows (incl.
|
|
59
|
+
"toda segunda-feira de junho às 9 da manhã").
|
|
60
|
+
- **Standalone single weekday recurrence (no following time) keeps the plural
|
|
61
|
+
article** "às segundas-feiras" (e.g. "a cada 15 minutos às segundas-feiras",
|
|
62
|
+
trailing-qualifier "… às segundas-feiras").
|
|
63
|
+
- **Lists carry the *-feira* suffix on the last *-feira* day only**, the
|
|
64
|
+
idiomatic pt-BR suffix-ellipsis: "às segundas, quartas e sextas-feiras";
|
|
65
|
+
"às terças, quintas-feiras, sábados e domingos" (terça bare, quinta is the
|
|
66
|
+
last *-feira* day so it carries the suffix, sábado/domingo never do). All
|
|
67
|
+
panels affirmed this is correct and unambiguous, not an inconsistency.
|
|
68
|
+
- **Ranges carry *-feira* on the last term only**: "de segunda a sexta-feira"
|
|
69
|
+
(the asymmetric form is the idiomatic pt-BR range shorthand — not
|
|
70
|
+
"de segunda-feira a sexta-feira").
|
|
71
|
+
- **Single weekday in an OR-union arm reads the Brazilian recurrence**
|
|
72
|
+
"às [weekday]s-feiras" / "aos domingos" (NOT "em qualquer [weekday]", which
|
|
73
|
+
reads slightly Iberian); a **range** arm keeps the nominal head
|
|
74
|
+
"em qualquer dia de segunda a sexta-feira" (a range needs the head "dia").
|
|
75
|
+
- **Quartz nth-weekday ordinal collision:** when the ordinal word would collide
|
|
76
|
+
with the weekday name ("segunda segunda-feira" for `1#2`), use the ordinal
|
|
77
|
+
digit "na 2ª segunda-feira do mês". Non-colliding ordinals keep the word form
|
|
78
|
+
("na última sexta-feira", "a primeira segunda-feira").
|
|
79
|
+
|
|
80
|
+
## Ordinals / dates
|
|
81
|
+
|
|
82
|
+
es: "el 1 de junio" / "el día N" / "el N de cada mes". **Decided for pt-BR:
|
|
83
|
+
"(no) dia 1 de junho"** — pt-BR routinely uses the cardinal with the noun *dia*
|
|
84
|
+
("dia 1", "dia 13"), so the donor's "el día N" maps cleanly to "dia N". Ranges:
|
|
85
|
+
"do dia 1 ao dia 15 do mês" (contractions *de+o=do*, *a+o=ao*). The es bare
|
|
86
|
+
"el 1 de junio" → "**dia 1 de junho**".
|
|
87
|
+
|
|
88
|
+
- **Decided (panel-confirmed): the 1st of the month is the ordinal "dia 1º"**;
|
|
89
|
+
every other day stays cardinal. The ordinal first is a deep pt-BR norm
|
|
90
|
+
(calendars, official/legal texts, speech); cardinal "dia 1" reads as a typo or
|
|
91
|
+
informal shorthand. The "1º" carries into the date-range and OR-union arms
|
|
92
|
+
too: "do dia 1º ao dia 15", "seja no dia 1º …". (Ranges carry the ordinal on
|
|
93
|
+
the first term and cardinal on the rest, the normal pt-BR pattern.) The
|
|
94
|
+
W-operator proximity preposition is the dative "próximo **ao** dia 15" (not
|
|
95
|
+
"próximo do dia 15") — proximity-to-a-target takes *a+o=ao*.
|
|
96
|
+
- Quartz nth-weekday ordinals: primeiro/primeira, segundo/segunda, terceiro,
|
|
97
|
+
quarto, quinto — **gendered** (see below).
|
|
98
|
+
|
|
99
|
+
## Contractions (the big es→pt divergence — renderer logic, not string swaps)
|
|
100
|
+
|
|
101
|
+
Portuguese fuses prepositions with the following article; the renderer must
|
|
102
|
+
form these wherever es emitted a bare preposition + article:
|
|
103
|
+
|
|
104
|
+
- *de* + o/a/os/as → **do / da / dos / das** ("do mês", "da manhã", "das 9").
|
|
105
|
+
- *em* + o/a/os/as → **no / na / nos / nas** ("no dia 1", "no minuto 30",
|
|
106
|
+
"na hora").
|
|
107
|
+
- *a* + a/as → **à / às** (clock and weekday recurrence; grave accent).
|
|
108
|
+
*a* + o/os → **ao / aos** (date ranges "ao dia 15").
|
|
109
|
+
- *por* generally stays separate in these phrasings (not needed as a fused
|
|
110
|
+
form for the cron domain; noted for completeness).
|
|
111
|
+
|
|
112
|
+
This contraction layer is the principal structural divergence from es and is
|
|
113
|
+
where most RED in the TDD port is expected — it is **gender/number-driven
|
|
114
|
+
formation**, not a lexical substitution.
|
|
115
|
+
|
|
116
|
+
## Connectives
|
|
117
|
+
|
|
118
|
+
- and → **e** (RAE-style coma ante "y" has **no pt-BR analog** — pt does *not*
|
|
119
|
+
put a comma before *e* in a simple series; **FLAGGED**: the donor's "coma
|
|
120
|
+
ante 'y'" re-strategy in the day-period join must be *dropped*, not ported.
|
|
121
|
+
This is a real renderer divergence, not a string swap.)
|
|
122
|
+
- or → **ou** (the OR-union connector; see re-strategies).
|
|
123
|
+
- range / until → **a** ("de … a …") and **até** where a terminal "until"
|
|
124
|
+
reads better; default to **a** to mirror es "de … a …".
|
|
125
|
+
|
|
126
|
+
## Names, gender, agreement
|
|
127
|
+
|
|
128
|
+
- Lowercase months and weekdays (confirmed pt-BR norm, VOLP).
|
|
129
|
+
- **Gender/agreement the renderer must handle (es→pt divergence):**
|
|
130
|
+
- Weekdays are **feminine** in pt (a segunda-feira) — the recurrence article
|
|
131
|
+
is *as* → *às*; es's masculine "los lunes" does not carry over. This drives
|
|
132
|
+
"às segundas-feiras", "qualquer segunda-feira".
|
|
133
|
+
- Quartz nth ordinals agree with the (feminine) weekday: "a primeira
|
|
134
|
+
segunda-feira", "o último domingo" (domingo masculine), "a última
|
|
135
|
+
sexta-feira". The renderer must select ordinal gender by weekday gender —
|
|
136
|
+
es used invariant "primer/último". **FLAGGED** as needing real agreement
|
|
137
|
+
logic.
|
|
138
|
+
- "todo(s)" / "cada" agreement: "todos os dias" (m.pl.), "cada mês" (m.),
|
|
139
|
+
"cada hora" (f.) — gendered determiners where es had "todos los días" /
|
|
140
|
+
"cada".
|
|
141
|
+
|
|
142
|
+
## Ported re-strategies (language-neutral; pt forms)
|
|
143
|
+
|
|
144
|
+
- **Per-hour windows for wildcard minutes over hour lists** (es §"wildcard
|
|
145
|
+
minutes over hour lists render as per-hour windows"): keep the strategy; pt
|
|
146
|
+
form "das 9 às 9:59 da manhã" (note *das* = de+as, *às* = a+as).
|
|
147
|
+
- **OR-union unified frame:** es "ya sea X o Y" → **"seja X ou Y"
|
|
148
|
+
(panel-confirmed).** All three personas read it as an unambiguous inclusive
|
|
149
|
+
OR; the "seja" frame is cleaner than a bare "X ou Y" and there is no
|
|
150
|
+
intersection misreading. "ou seja" is avoided — it means "that is/i.e." The
|
|
151
|
+
shared month is fronted once and the arms are month-less, exactly as in es.
|
|
152
|
+
The weekday arm wording is resolved under *Weekday recurrence* above (single
|
|
153
|
+
weekday → "às [weekday]s-feiras"; range → "em qualquer dia de segunda a
|
|
154
|
+
sexta-feira").
|
|
155
|
+
- **No-fold month range:** a month range never folds into another phrase
|
|
156
|
+
("dia 1 de junho a setembro" parses as "(dia 1 de junho) a setembro"); dates
|
|
157
|
+
scope it instead ("dia 1 de cada mês, de junho a setembro"); mixed lists
|
|
158
|
+
repeat the preposition per piece ("em janeiro e de março a junho"). Same rule
|
|
159
|
+
as es and English.
|
|
160
|
+
- **Step-flattening:** step segments inside lists always flatten into their
|
|
161
|
+
fires — months, weekdays, dates, minutes, seconds — no raw step token reaches
|
|
162
|
+
the output. Identical to es.
|
|
163
|
+
- **Anchored minutes/seconds** read as "no minuto 30 de cada hora" (em+o=no),
|
|
164
|
+
the donor's "en el minuto 30 de cada hora" — not a calque of "past the hour".
|
|
165
|
+
|
|
166
|
+
## Dialect axis (future)
|
|
167
|
+
|
|
168
|
+
pt-PT is a **future dialect** (clock/lexical divergences from pt-BR, e.g. some
|
|
169
|
+
date/register differences), mirroring es's es-ES / es-419 split. **One `pt`
|
|
170
|
+
table today = pt-BR.** A future `pt-PT` (and any regional pt-BR style such as a
|
|
171
|
+
"9h" colloquial-clock custom field) would clear its own native panel before
|
|
172
|
+
shipping, per the dialect rules in the pipeline.
|
|
173
|
+
|
|
174
|
+
## Residuals inherited from es (NOT fixed here — es+pt follow-up)
|
|
175
|
+
|
|
176
|
+
The blind pt-BR panel's technical reviewer flagged two issues that are **shared
|
|
177
|
+
artifacts of the es donor corpus**, not pt regressions, so they were left in the
|
|
178
|
+
pt corpus to keep it field-comparable with es and are tracked as a joint es+pt
|
|
179
|
+
follow-up (docs/backlog.md, per-language follow-ups):
|
|
180
|
+
|
|
181
|
+
- **Hour-window overlap in `* 2/4,18-20 * * *`.** Hour 18 is named twice — once
|
|
182
|
+
as the 2/4 step arm's per-hour window ("das 6 às 6:59 da tarde") and again as
|
|
183
|
+
the left endpoint of the 18-20 range window ("das 6 da tarde às 8:59 da
|
|
184
|
+
noite"). The fire set is correct (no value dropped or understated); the
|
|
185
|
+
overlap is a rendering-clarity artifact present identically in es.
|
|
186
|
+
- **OR DOW-arm "e" bracketing** in `… ou de segunda a sexta-feira e aos
|
|
187
|
+
domingos` (`0 0 1 * 0,1-5`, `0 0 1 6-9 0,1-5`). The internal "e" joining
|
|
188
|
+
Mon–Fri + Sun inside the second OR arm could be misparsed as a top-level
|
|
189
|
+
conjunction. The meaning is correct and the construction is the same one the
|
|
190
|
+
es donor uses ("o de lunes a viernes y los domingos"); fixing it is an es+pt
|
|
191
|
+
bracketing change, not a pt-only one.
|
|
192
|
+
|
|
193
|
+
## Known trade-offs
|
|
194
|
+
|
|
195
|
+
- `short` only switches spelled numbers to digits; pt name abbreviations
|
|
196
|
+
(seg., qua.) are not yet implemented (same residue as es).
|
|
197
|
+
- The grave-accent contraction (à/às) is correctness-critical for the 1-o'clock
|
|
198
|
+
and weekday-recurrence forms; the renderer forms it programmatically rather
|
|
199
|
+
than hard-coding strings.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Portuguese",
|
|
3
|
+
"status": "beta",
|
|
4
|
+
"humanReview": null,
|
|
5
|
+
"modelReview": "sibling-derived from es (the validated Romance sibling): the es renderer's structure was ported (plan override, OR-union frame, parity predicates, re-strategies, dialect scaffold) and its lexicon translated to pt-BR, then TDD'd to green against the reviewed pt-BR corpus (277 entries). The corpus was a candidate translated from the reviewed es corpus and finalized by a blind 3-persona pt-BR Sonnet panel (everyday / copy-editor / technical) before the port (corpus -> review -> port; tooling/docs/language-pipeline.md). The es->pt divergences the panel and TDD surfaced and the renderer now handles: preposition+article contraction (do/da/no/na/à/às/ao/aos, formed gender/number-driven, not as strings), gender agreement (feminine -feira weekdays vs masculine domingo/sábado, gendered Quartz ordinals and the 'duas horas' feminine cardinal), the 'toda X' single-feminine-weekday recurrence head (kills the double-'às'), the suffix-ellipsis weekday list ('às segundas, quartas e sextas-feiras'), 'na 2ª segunda-feira' (ordinal digit on the weekday-name collision), the dropped comma before 'e', and the ordinal 'dia 1º' for the 1st of the month. Objective gates (round-trip, fuzz dropped-value detector, both-side OR-scope, cRonstrue pt_BR reference) plus the panel gate beta.",
|
|
6
|
+
"note": "BETA — model-validated: sibling-derived from es (a proven structure/style anchor), TDD-green over a blind-panel-reviewed pt-BR corpus, and clean on the corpus-independent mechanical gates (fuzz, round-trip, OR-scope, cRonstrue pt_BR). Two residuals are inherited unchanged from the es donor and are NOT pt regressions (tracked as a joint es+pt follow-up; notes.md §Residuals): (1) the hour-window overlap in '* 2/4,18-20 * * *' (hour 18 named twice — the rendering-clarity artifact, no value dropped); (2) the OR DOW-arm 'e' bracketing in '… ou de segunda a sexta-feira e aos domingos'. The 'short' style switches spelled numbers to digits but pt name abbreviations are not yet implemented (same residue as es). Graduates to stable only on fluent-pt human review. pt-PT is a future dialect axis (notes.md §Dialect axis); no regional dialect ships yet.",
|
|
7
|
+
"dialects": {}
|
|
8
|
+
}
|
package/src/lang/zh/index.ts
CHANGED
|
@@ -25,6 +25,40 @@ type StepSegment = Extract<Segment, {kind: 'step'}>;
|
|
|
25
25
|
const UNITS = {hour: '小时', minute: '分钟', second: '秒'};
|
|
26
26
|
const WEEKDAYS = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
|
27
27
|
|
|
28
|
+
// Simplified → Traditional (zh-Hant) Han glyph map. Schedule prose differs
|
|
29
|
+
// between the two scripts only by character form — within this domain every
|
|
30
|
+
// Simplified glyph that has a Traditional form maps 1:1 with no context
|
|
31
|
+
// sensitivity — so the Traditional variant is the reviewed Simplified output
|
|
32
|
+
// with this map applied at the render boundary, NOT a second word table that
|
|
33
|
+
// would duplicate the renderer's logic. The Taiwan-standard form is chosen for
|
|
34
|
+
// each glyph (週 for week, 點/時/鐘/個/數/單/雙/後/間/從/內); 啟 (not the 啓
|
|
35
|
+
// variant) for 啟動. Two whole-word choices are kept faithful to the 1:1 map
|
|
36
|
+
// and flagged for native review in notes.md: 運行時間 (a Taiwan-native may say
|
|
37
|
+
// 執行時間) and 表達式 (Taiwan tech register may prefer 運算式 / 表示式).
|
|
38
|
+
const HANT: {[glyph: string]: string} = {
|
|
39
|
+
个: '個', 从: '從', 内: '內', 别: '別', 动: '動', 单: '單', 双: '雙',
|
|
40
|
+
后: '後', 启: '啟', 周: '週', 数: '數', 无: '無', 时: '時', 点: '點',
|
|
41
|
+
统: '統', 识: '識', 达: '達', 运: '運', 钟: '鐘', 间: '間'
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Apply the Traditional glyph map to a finished Simplified string. The default
|
|
45
|
+
// Simplified (zh / zh-Hans) variant returns the input untouched, so its output
|
|
46
|
+
// is byte-identical to before this variant existed.
|
|
47
|
+
function toVariant(text: string, variant: ChineseStyle['variant']): string {
|
|
48
|
+
if (variant !== 'Hant') {
|
|
49
|
+
return text;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return Array.from(text, (glyph) => HANT[glyph] ?? glyph).join('');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The variant the most recent `options()` call resolved. `cronli5()` always
|
|
56
|
+
// calls `options()` before reading `reboot`/`fallback` or invoking `sentence`,
|
|
57
|
+
// none of which receive `opts`; this lets those contract-fixed members honor
|
|
58
|
+
// the dialect without changing the shared Language contract. The library is
|
|
59
|
+
// synchronous and single render per call, so a module-private latch is safe.
|
|
60
|
+
let activeVariant: ChineseStyle['variant'] = 'Hans';
|
|
61
|
+
|
|
28
62
|
// "A、B和C" — enumerate with 、 and join the final item with 和.
|
|
29
63
|
function joinAnd(items: string[]): string {
|
|
30
64
|
if (items.length < 2) {
|
|
@@ -67,7 +101,7 @@ function renderStride(stride: Stride): string {
|
|
|
67
101
|
const {interval, start, last, cycle, unit, mark, anchor} = stride;
|
|
68
102
|
const lead = anchor + '从' + start + mark + '起' + cadence(interval, unit);
|
|
69
103
|
|
|
70
|
-
return chooseStride({start, interval, cycle}, {
|
|
104
|
+
return chooseStride({start, interval, last, cycle}, {
|
|
71
105
|
bare: () => cadence(interval, unit),
|
|
72
106
|
offset: () => lead,
|
|
73
107
|
bounded: () => lead + ',至' + last + mark
|
|
@@ -1041,8 +1075,26 @@ function render(schedule: Schedule, plan: PlanNode, opts: Opts): string {
|
|
|
1041
1075
|
|
|
1042
1076
|
// --- Day-level qualifier (date / month / weekday / year). ---
|
|
1043
1077
|
|
|
1044
|
-
//
|
|
1078
|
+
// Whether the month is a BOUNDED parity step ("2-10/2") — an interval-2 step
|
|
1079
|
+
// that does NOT span the open parity set. It enumerates as a list of singles
|
|
1080
|
+
// ("2、4、6、8、10月"), so it takes the multi-month comma like an explicit list,
|
|
1081
|
+
// unlike a single month or a non-parity bounded step ("3-11/3", glued).
|
|
1082
|
+
function boundedParityMonth(schedule: Schedule): boolean {
|
|
1083
|
+
if (schedule.shapes.month !== 'step' ||
|
|
1084
|
+
isOpenStep(schedule.pattern.month)) {
|
|
1085
|
+
return false;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
const segs = segmentsOf(schedule, 'month');
|
|
1089
|
+
|
|
1090
|
+
return segs.length === 1 && segs[0].kind === 'step' && segs[0].interval === 2;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// The month phrase: "" (wildcard), "每个奇数月"/"每个偶数月" (OPEN step ×2),
|
|
1045
1094
|
// "1月至3月" (range), else the enumerated numbers sharing one 月 ("1、4、7、10月").
|
|
1095
|
+
// A BOUNDED parity step ("2-10/2" = months 2,4,6,8,10) fires a finite set, so
|
|
1096
|
+
// it enumerates through the number path below rather than the open parity class
|
|
1097
|
+
// — the "每个偶数月" wording asserts December too, which the bound excludes.
|
|
1046
1098
|
function monthPhrase(schedule: Schedule): string {
|
|
1047
1099
|
if (schedule.pattern.month === '*') {
|
|
1048
1100
|
return '';
|
|
@@ -1051,7 +1103,8 @@ function monthPhrase(schedule: Schedule): string {
|
|
|
1051
1103
|
const segs = segmentsOf(schedule, 'month');
|
|
1052
1104
|
const first = segs[0];
|
|
1053
1105
|
|
|
1054
|
-
if (segs.length === 1 && first.kind === 'step' && first.interval === 2
|
|
1106
|
+
if (segs.length === 1 && first.kind === 'step' && first.interval === 2 &&
|
|
1107
|
+
isOpenStep(schedule.pattern.month)) {
|
|
1055
1108
|
return '每个' + (first.fires[0] % 2 ? '奇' : '偶') + '数月';
|
|
1056
1109
|
}
|
|
1057
1110
|
|
|
@@ -1074,13 +1127,14 @@ function monthPhrase(schedule: Schedule): string {
|
|
|
1074
1127
|
return nums.join('、') + '月';
|
|
1075
1128
|
}
|
|
1076
1129
|
|
|
1077
|
-
// The day-of-month list. A
|
|
1078
|
-
// (
|
|
1130
|
+
// The day-of-month list. A list of singles — or a bounded step enumerated to
|
|
1131
|
+
// its fires (9-17/2 = 9,11,13,15,17) — shares one trailing 日 ("1、3、8日",
|
|
1132
|
+
// "9、11、13、15、17日"); any range gives each segment its own 日 ("1至5日、10日").
|
|
1079
1133
|
function dayList(schedule: Schedule): string {
|
|
1080
1134
|
const segs = segmentsOf(schedule, 'date');
|
|
1081
1135
|
|
|
1082
|
-
if (segs.every((seg) => seg.kind === 'single')) {
|
|
1083
|
-
return segs
|
|
1136
|
+
if (segs.every((seg) => seg.kind === 'single' || seg.kind === 'step')) {
|
|
1137
|
+
return fireValues(segs).join('、') + '日';
|
|
1084
1138
|
}
|
|
1085
1139
|
|
|
1086
1140
|
return segs.map(function day(seg) {
|
|
@@ -1088,7 +1142,9 @@ function dayList(schedule: Schedule): string {
|
|
|
1088
1142
|
return seg.bounds[0] + '日至' + seg.bounds[1] + '日';
|
|
1089
1143
|
}
|
|
1090
1144
|
|
|
1091
|
-
return
|
|
1145
|
+
return seg.kind === 'step' ?
|
|
1146
|
+
seg.fires.join('、') + '日' :
|
|
1147
|
+
(seg as {value: string}).value + '日';
|
|
1092
1148
|
}).join('、');
|
|
1093
1149
|
}
|
|
1094
1150
|
|
|
@@ -1152,7 +1208,11 @@ function datePhrase(schedule: Schedule): string {
|
|
|
1152
1208
|
return quartzDate(date, month || '本月');
|
|
1153
1209
|
}
|
|
1154
1210
|
|
|
1155
|
-
|
|
1211
|
+
// An OPEN day step ("*/N") is a frequency — the bare "每N天" cadence. A
|
|
1212
|
+
// BOUNDED step ("a-b/N") fires a finite set of days, so it enumerates them
|
|
1213
|
+
// through the day-list path below, never the cadence (which would drop the
|
|
1214
|
+
// bounds).
|
|
1215
|
+
if (schedule.shapes.date === 'step' && isOpenStep(date)) {
|
|
1156
1216
|
return month + cadence(stepSegment(schedule, 'date').interval, '天');
|
|
1157
1217
|
}
|
|
1158
1218
|
|
|
@@ -1160,12 +1220,13 @@ function datePhrase(schedule: Schedule): string {
|
|
|
1160
1220
|
return '每月' + dayList(schedule);
|
|
1161
1221
|
}
|
|
1162
1222
|
|
|
1163
|
-
// A multi-month scope (range/list
|
|
1223
|
+
// A multi-month scope (range/list, or a bounded parity step that enumerates
|
|
1224
|
+
// like a list — "2、4、6、8、10月") ends in 月 and would run straight into the
|
|
1164
1225
|
// day — "6月至8月1日" reads "8月1日" as August 1st. The comma keeps the month
|
|
1165
1226
|
// scope distinct from the day ("6月至8月,1日"). A single month stays glued
|
|
1166
1227
|
// ("6月1日"), which is unambiguous.
|
|
1167
1228
|
const monthMulti = schedule.shapes.month === 'range' ||
|
|
1168
|
-
schedule.shapes.month === 'list';
|
|
1229
|
+
schedule.shapes.month === 'list' || boundedParityMonth(schedule);
|
|
1169
1230
|
|
|
1170
1231
|
return month + (monthMulti ? ',' : '') + dayList(schedule);
|
|
1171
1232
|
}
|
|
@@ -1178,7 +1239,9 @@ function dateCore(schedule: Schedule, quartzPrefix: string): string {
|
|
|
1178
1239
|
return quartzDate(schedule.pattern.date, quartzPrefix);
|
|
1179
1240
|
}
|
|
1180
1241
|
|
|
1181
|
-
|
|
1242
|
+
// An open day step is the bare "每N天" cadence; a bounded step enumerates its
|
|
1243
|
+
// days through dayList (see datePhrase), so the bounds are not dropped.
|
|
1244
|
+
if (schedule.shapes.date === 'step' && isOpenStep(schedule.pattern.date)) {
|
|
1182
1245
|
return cadence(stepSegment(schedule, 'date').interval, '天');
|
|
1183
1246
|
}
|
|
1184
1247
|
|
|
@@ -1318,7 +1381,12 @@ function composePoint(schedule: Schedule, core: string): string {
|
|
|
1318
1381
|
|
|
1319
1382
|
const dateSet = isSet(schedule.pattern.date);
|
|
1320
1383
|
const weekdaySet = isSet(schedule.pattern.weekday);
|
|
1321
|
-
|
|
1384
|
+
// The comma separates an OR union or the open "每N天" cadence from the core. A
|
|
1385
|
+
// bounded date step renders as a glued day list ("每月9、11…日"), not a
|
|
1386
|
+
// cadence, so it takes no comma — only an open step does.
|
|
1387
|
+
const dateCadence = schedule.shapes.date === 'step' &&
|
|
1388
|
+
isOpenStep(schedule.pattern.date);
|
|
1389
|
+
const comma = dateSet && weekdaySet || dateCadence;
|
|
1322
1390
|
|
|
1323
1391
|
return qual + (comma ? ',' : '') + core;
|
|
1324
1392
|
}
|
|
@@ -1371,6 +1439,13 @@ function hourCadenceApplies(schedule: Schedule): boolean {
|
|
|
1371
1439
|
}
|
|
1372
1440
|
|
|
1373
1441
|
function describe(schedule: Schedule, opts: Opts): string {
|
|
1442
|
+
return toVariant(describeHans(schedule, opts), opts.style.variant);
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
// The Simplified rendering of a schedule; `describe` maps it to the active
|
|
1446
|
+
// variant. The body owns every Simplified glyph; the variant is applied once,
|
|
1447
|
+
// at the boundary, so no emit point has to know which script it is writing.
|
|
1448
|
+
function describeHans(schedule: Schedule, opts: Opts): string {
|
|
1374
1449
|
const {kind} = schedule.plan;
|
|
1375
1450
|
const core = render(schedule, schedule.plan, opts);
|
|
1376
1451
|
let composed = core;
|
|
@@ -1419,22 +1494,36 @@ function describe(schedule: Schedule, opts: Opts): string {
|
|
|
1419
1494
|
function normalizeOptions(options?: Cronli5Options): Opts {
|
|
1420
1495
|
options = options || {};
|
|
1421
1496
|
|
|
1497
|
+
const style = resolveDialect(options.dialect);
|
|
1498
|
+
|
|
1499
|
+
// `cronli5()` reads `reboot`/`fallback` and calls `sentence` without `opts`;
|
|
1500
|
+
// latch the variant here (always called first) so they can honor the dialect.
|
|
1501
|
+
activeVariant = style.variant;
|
|
1502
|
+
|
|
1422
1503
|
return {
|
|
1423
1504
|
ampm: typeof options.ampm === 'boolean' ? options.ampm : false,
|
|
1424
1505
|
lenient: !!options.lenient,
|
|
1425
1506
|
seconds: !!options.seconds,
|
|
1426
1507
|
short: !!options.short,
|
|
1427
|
-
style
|
|
1508
|
+
style,
|
|
1428
1509
|
years: !!options.years
|
|
1429
1510
|
};
|
|
1430
1511
|
}
|
|
1431
1512
|
|
|
1432
1513
|
const zh: Language<ChineseStyle> = {
|
|
1433
1514
|
describe,
|
|
1434
|
-
fallback
|
|
1515
|
+
// `reboot`/`fallback` are contract-fixed strings the core reads without
|
|
1516
|
+
// `opts`; getters honor the variant `options()` latched, keeping the shared
|
|
1517
|
+
// Language contract unchanged while the Traditional dialect still applies.
|
|
1518
|
+
get fallback(): string {
|
|
1519
|
+
return toVariant('无法识别的 cron 表达式', activeVariant);
|
|
1520
|
+
},
|
|
1435
1521
|
options: normalizeOptions,
|
|
1436
|
-
reboot:
|
|
1437
|
-
|
|
1522
|
+
get reboot(): string {
|
|
1523
|
+
return toVariant('系统启动时', activeVariant);
|
|
1524
|
+
},
|
|
1525
|
+
sentence: (description) =>
|
|
1526
|
+
toVariant('运行时间:', activeVariant) + description + '。'
|
|
1438
1527
|
};
|
|
1439
1528
|
|
|
1440
1529
|
export default zh;
|
package/src/lang/zh/notes.md
CHANGED
|
@@ -22,9 +22,21 @@ conventions (this doc) → coverage-spec pattern set → panel-validated
|
|
|
22
22
|
for duration vs 分 for clock position (never swapped); **每天** not 每日;
|
|
23
23
|
suppress the numeral 1 (每分钟, not 每1分钟); no 第 before day/month numbers;
|
|
24
24
|
no redundant 每.
|
|
25
|
-
- **Simplified (zh-Hans) is the default; Traditional (zh-Hant) is a
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
- **Simplified (zh-Hans) is the default; Traditional (zh-Hant) is a within-zh
|
|
26
|
+
variant** selected by the `dialect` option, NOT a separate top-level locale.
|
|
27
|
+
A separate `zh-Hant` module would have to duplicate or import zh's assembly
|
|
28
|
+
logic, both forbidden (a language never imports another language — see
|
|
29
|
+
docs/i18n-design.md; only the core is shared). Within this domain the two
|
|
30
|
+
scripts differ only by character form (no register-level grammar split), so
|
|
31
|
+
the variant is the reviewed Simplified output with a 1:1 Han glyph map applied
|
|
32
|
+
at the render boundary (`toVariant` in `index.ts`:
|
|
33
|
+
時/鐘/點/週/個/數/單/雙/後/間/從/內/無/識/別/啟/統/達/運). It ships
|
|
34
|
+
**experimental** — a model-drafted glyph/register mapping, not yet validated
|
|
35
|
+
by a Traditional-native or blind Hant panel (the same gate that graduates zh).
|
|
36
|
+
Two whole-word choices are flagged for native review: `運行時間` (a
|
|
37
|
+
Taiwan-native may say `執行時間`) and `表達式` (Taiwan tech register may prefer
|
|
38
|
+
`運算式` / `表示式`); both are widely-accepted, and the faithful 1:1 map is
|
|
39
|
+
kept so the variant stays a pure transliteration of the reviewed Hans oracle.
|
|
28
40
|
- **Confinement uses a frame, never juxtaposed cadences:** 在9点至17点之间,
|
|
29
41
|
每15分钟 — the 在…之间 frame binds the cadence to the window (the same
|
|
30
42
|
confinement-vs-juxtaposition rule as the other languages).
|
|
@@ -95,7 +107,7 @@ every field value preserved — `npm run fuzz zh` is clean):
|
|
|
95
107
|
|---|---|---|---|
|
|
96
108
|
| `numerals` | `'arabic'` / `'chinese'` | arabic | 9点 vs 九点 |
|
|
97
109
|
| `clock` | `'24h'` / `'12h'` | 24h | 14点 vs 下午2点 (maps to today's `ampm`) |
|
|
98
|
-
| `
|
|
110
|
+
| `dialect` | `'zh-Hans'` / `'zh-Hant'` | zh-Hans | Simplified vs Traditional Han glyph form (within-zh variant; experimental) |
|
|
99
111
|
| `quarterHour` | bool | false | enable 半 / 一刻 / 三刻 (default is explicit 分) |
|
|
100
112
|
| `useHao` | bool | false | 号 vs 日 |
|
|
101
113
|
|
package/src/lang/zh/status.json
CHANGED
|
@@ -3,5 +3,14 @@
|
|
|
3
3
|
"status": "beta",
|
|
4
4
|
"humanReview": null,
|
|
5
5
|
"modelReview": "blind 3-persona Sonnet style panel + author/audit corpus workflow (2026-06-20); npm run fuzz zh clean (0 throws / degenerate / missing-value)",
|
|
6
|
-
"note": "BETA — model-validated by a blind 3-persona Sonnet panel (src/lang/zh/notes.md), corpus authored via an author/audit/fix workflow and converged to the renderer's canonical forms. Graduates to stable only on fluent-human review."
|
|
6
|
+
"note": "BETA — model-validated by a blind 3-persona Sonnet panel (src/lang/zh/notes.md), corpus authored via an author/audit/fix workflow and converged to the renderer's canonical forms. Graduates to stable only on fluent-human review.",
|
|
7
|
+
"variants": {
|
|
8
|
+
"zh-Hant": {
|
|
9
|
+
"name": "Chinese (Mandarin, Traditional)",
|
|
10
|
+
"status": "experimental",
|
|
11
|
+
"humanReview": null,
|
|
12
|
+
"modelReview": null,
|
|
13
|
+
"note": "EXPERIMENTAL — a within-zh variant (the reviewed Simplified output with a 1:1 Han glyph map applied at the render boundary, selected by {dialect: 'zh-Hant'}), NOT a separate locale. Model-drafted glyph/register mapping, not yet validated by a Traditional-native or blind Hant panel. Graduates to beta on a Traditional-native / blind Hant review (the same gate model as Simplified zh). Whole-word choices flagged for native review: 運行時間 (vs 執行時間), 表達式 (vs 運算式 / 表示式)."
|
|
14
|
+
}
|
|
15
|
+
}
|
|
7
16
|
}
|
package/types/core/cadence.d.ts
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Cronli5Options } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* French's own resolved style shape: a separator and the spacing of the `h`
|
|
4
|
+
* clock mark. fr is 24-hour only, so there is no `ampm`/`meridiem` axis.
|
|
5
|
+
*/
|
|
6
|
+
export interface FrenchStyle {
|
|
7
|
+
sep: string;
|
|
8
|
+
unspaced: boolean;
|
|
9
|
+
}
|
|
10
|
+
declare function resolveDialect(dialect: Cronli5Options['dialect']): FrenchStyle;
|
|
11
|
+
export { resolveDialect };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Cronli5Options } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Portuguese's own resolved style shape has a separator,
|
|
4
|
+
* clock default, meridiem form, and `h` suffix.
|
|
5
|
+
*/
|
|
6
|
+
export interface PortugueseStyle {
|
|
7
|
+
ampm: boolean;
|
|
8
|
+
hSuffix: boolean;
|
|
9
|
+
meridiem: 'descriptors' | 'english';
|
|
10
|
+
sep: string;
|
|
11
|
+
}
|
|
12
|
+
declare function resolveDialect(dialect: Cronli5Options['dialect']): PortugueseStyle;
|
|
13
|
+
export { resolveDialect };
|