cronli5 0.3.4 → 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.
@@ -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
+ }
@@ -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
@@ -1405,6 +1439,13 @@ function hourCadenceApplies(schedule: Schedule): boolean {
1405
1439
  }
1406
1440
 
1407
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 {
1408
1449
  const {kind} = schedule.plan;
1409
1450
  const core = render(schedule, schedule.plan, opts);
1410
1451
  let composed = core;
@@ -1453,22 +1494,36 @@ function describe(schedule: Schedule, opts: Opts): string {
1453
1494
  function normalizeOptions(options?: Cronli5Options): Opts {
1454
1495
  options = options || {};
1455
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
+
1456
1503
  return {
1457
1504
  ampm: typeof options.ampm === 'boolean' ? options.ampm : false,
1458
1505
  lenient: !!options.lenient,
1459
1506
  seconds: !!options.seconds,
1460
1507
  short: !!options.short,
1461
- style: resolveDialect(options.dialect),
1508
+ style,
1462
1509
  years: !!options.years
1463
1510
  };
1464
1511
  }
1465
1512
 
1466
1513
  const zh: Language<ChineseStyle> = {
1467
1514
  describe,
1468
- fallback: '无法识别的 cron 表达式',
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
+ },
1469
1521
  options: normalizeOptions,
1470
- reboot: '系统启动时',
1471
- sentence: (description) => '运行时间:' + description + '。'
1522
+ get reboot(): string {
1523
+ return toVariant('系统启动时', activeVariant);
1524
+ },
1525
+ sentence: (description) =>
1526
+ toVariant('运行时间:', activeVariant) + description + '。'
1472
1527
  };
1473
1528
 
1474
1529
  export default zh;
@@ -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 separate
26
- top-level locale**, not a sub-dialect a distinct vocab/surface table
27
- (週/號 swaps), per the copy-editor.
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
- | `locale` | `'zh-Hans'` / `'zh-Hant'` | zh-Hans | Simplified vs Traditional vocab table |
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
 
@@ -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
  }
@@ -22,6 +22,7 @@ interface StrideParts {
22
22
  declare function renderStride(spec: {
23
23
  start: number;
24
24
  interval: number;
25
+ last: number;
25
26
  cycle: number;
26
27
  }, parts: StrideParts): string;
27
28
  declare function hourListStride(values: number[]): {
@@ -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,4 @@
1
+ import type { Language } from '../../core/schedule.js';
2
+ import { type FrenchStyle } from './dialects.js';
3
+ declare const fr: Language<FrenchStyle>;
4
+ export default fr;
@@ -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 };
@@ -0,0 +1,4 @@
1
+ import type { Language } from '../../core/schedule.js';
2
+ import { type PortugueseStyle } from './dialects.js';
3
+ declare const pt: Language<PortugueseStyle>;
4
+ export default pt;