Skip to content

Commit cf0a04c

Browse files
PR 4th review
1 parent 3460b79 commit cf0a04c

4 files changed

Lines changed: 53 additions & 15 deletions

File tree

packages/tempo/doc/tempo.cookbook.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ new Tempo('tomorrow afternoon');
101101

102102
::: tip
103103
**Looking for Internationalized Parsing?**
104-
Tempo can automatically translate months, weekdays, and relative terms (like 'yesterday', 'today', 'tomorrow') into foreign languages using your `locale` configuration. See the [Smart Parsing Guide](./tempo.parse.md#internationalized-parsing-locales) for full documentation and current capabilities.
104+
Tempo can automatically translate months, weekdays, and relative terms (like 'yesterday', 'today', 'tomorrow') into foreign languages using your `locale` configuration. This requires enabling the parser option `parse: { localize: true }` (or the top-level `localize: true` flag) alongside your locale setting. See the [Smart Parsing Guide](./tempo.parse.md#internationalized-parsing-locales) for full documentation and current capabilities.
105105
:::
106106

107107
### Parsing Unix Timestamps

packages/tempo/doc/tempo.parse.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,10 @@ Tempo can be instructed to automatically generate language-specific parsing rule
131131
```typescript
132132
Tempo.init({ locale: 'fr-FR', parse: { localize: true } });
133133

134-
// Natively understand French input out-of-the-box!
134+
// Natively understand French dates and core events!
135135
new Tempo('demain'); // parses as "tomorrow"
136136
new Tempo('15 fevrier 2026'); // parses as "15 February 2026"
137-
new Tempo('vendredi prochain'); // parses as "next Friday"
137+
new Tempo('vendredi'); // parses as the closest "Friday"
138138
```
139139

140140
#### How it Works & Accent Normalization
@@ -144,24 +144,30 @@ It also automatically **normalizes and strips accents** from these generated rul
144144

145145
#### ⚠️ Current Limitations (What is NOT Available)
146146
While `Intl` provides a robust foundation for month and weekday translations, there are limits to auto-localization:
147-
* **English Affixes**: Grammatical connector words like "ago", "next", "last", "in", and "from now" are heavily English-biased syntax rules. `Intl` does not provide translations for these parsing connectors, meaning `2 days ago` will only parse correctly if the keyword `ago` is used.
147+
* **English Affixes**: Grammatical connector words like "ago", "next", "last", "in", and "from now" are heavily English-biased syntax rules. `Intl` does not provide translations for these parsing connectors. When using the `Tempo` constructor with `parse: { localize: true }`, a relative string like `2 days ago` or `next Friday` will only parse correctly if the English connector keywords (`ago`, `next`) are used, unless Custom Aliases are used to bridge the gap.
148148
* **Time Units**: Words representing time units ("days", "weeks", "months") inside natural language strings are currently English-only.
149149
* **Grammar Structure**: The parser expects sequences matching standard English formats (e.g., `[value] [unit] [affix]`). Highly inflected languages or completely different phrase structures might fail to parse.
150150

151151
To bridge these gaps, you can register **Custom Aliases** (see below) to map foreign syntax to specific relative offsets manually!
152152

153153
### Custom Aliases (Events & Periods)
154-
You can teach the parser new words:
154+
You can teach the parser new words or entire foreign phrases to bridge translation gaps:
155155

156156
```typescript
157157
Tempo.init({
158+
locale: 'fr-FR',
159+
parse: { localize: true },
158160
event: {
161+
// Map a full foreign phrase directly to an English-equivalent relative string
162+
'vendredi prochain': () => 'next Friday',
163+
// Or standard static events
159164
'launch': '2026-12-01',
160165
'party': () => 'next Friday 8pm'
161166
}
162167
});
163168

164-
const t = new Tempo('party');
169+
const t1 = new Tempo('vendredi prochain'); // Parses accurately to next Friday
170+
const t2 = new Tempo('party');
165171
```
166172

167173
### 🧠 Functional Alias Context
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Localized Mathematical Modifiers
2+
3+
## Objective
4+
Enable full localization of mathematical modifier terms (e.g., mapping `"prochain"` to `">"` or `"next"`) and gracefully handle grammatical structure variations, such as inverted word ordering (e.g., trailing modifiers like `[weekday] [modifier]` vs. the English default `[modifier] [weekday]`).
5+
6+
## Architectural Considerations
7+
8+
### 1. Decoupling Math from Hardcoded English
9+
Currently, the `parseModifier` function in `engine.lexer.ts` uses a strict `switch` statement that evaluates literal English strings (e.g., `case 'next': return 1`).
10+
- **Challenge**: Passing foreign strings like `"prochain"` directly to this switch fails and defaults to `0`.
11+
- **Solution Space**: Introduce a pre-lexing normalization step or a `modifier` registry that maps foreign string literals to standard internal mathematical tokens (like `>`, `<`, `+`, `-`) before they hit the mathematical evaluator.
12+
13+
### 2. Lexer & Master Guard Layout Flexibility
14+
Tempo’s `Token.wkd` and standard layouts (e.g., `Pattern.WkdTime`) currently expect modifiers in specific positions (often as prefixes, with limited hardcoded suffixes like `next|last` for English).
15+
- **Challenge**: When `parse: { localize: true }` is enabled, the localized snippet overrides completely drop trailing suffix captures.
16+
- **Solution Space**: Update `support.init.ts` and `support.default.ts` to dynamically generate both prefix and suffix capture groups (`<mod_pre>` and `<mod_suf>`) in the localized regexes, allowing the parser to extract the modifier regardless of which side of the noun it appears.
17+
18+
### 3. Locale-Specific Grammatical Nuances
19+
Different languages place modifiers in different structural positions depending on the entity.
20+
- **Challenge**: A language might use a suffix for days (e.g., "vendredi prochain") but a prefix for other temporal periods.
21+
- **Solution Space**: Should structural expectations be strictly tied to `Intl` locale codes, or should the engine use a "greedy" approach where it just attempts to extract modifiers from either side of the token without strictly enforcing grammatical correctness?
22+
23+
### 4. Configuration API Design
24+
How will developers interact with this new capability?
25+
- **Option A**: A brand new top-level configuration registry: `Tempo.init({ modifier: { 'prochain': 'next', 'dernier': 'last' } })`.
26+
- **Option B**: Expanding the existing `event` or `snippet` objects.
27+
- **Option C**: Can we extract these modifier words automatically from `Intl.RelativeTimeFormat`? (Investigate if `Intl` provides sufficient grammatical connector data).
28+
29+
### 5. Performance Implications
30+
The core speed of Tempo relies heavily on Master Guard (RegEx) optimization and caching.
31+
- **Challenge**: Adding multiple optional prefix and suffix capture branches to core snippets (like `wkd` and `rel`) will increase the complexity and backtracking potential of the Master Guard patterns.
32+
- **Solution Space**: Ensure careful benchmarking when adding dynamic `<sfx>` groups to localized patterns.

packages/tempo/src/tempo.class.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -294,13 +294,10 @@ export class Tempo {
294294

295295
/** get first Canonical name of a supplied locale */
296296
private static _locale = (locale?: string) => {
297+
const global = Context.global;
297298
let language: string | undefined;
298299

299-
try { // lookup locale
300-
language = canonicalLocale(locale!);
301-
} catch (error) { } // catch unknown locale
302-
303-
const global = Context.global;
300+
if (locale) language = canonicalLocale(locale);
304301

305302
return language ??
306303
global?.navigator?.languages?.[0] ?? // fallback to current first navigator.languages[]
@@ -320,7 +317,6 @@ export class Tempo {
320317
? Object.assign(Tempo.readStore(storeKey), providedOptions)
321318
: providedOptions;
322319

323-
console.log('[$setConfig] providedOptions:', providedOptions, 'mergedOptions:', mergedOptions, 'isEmpty:', isEmpty(mergedOptions));
324320
if (isEmpty(mergedOptions)) return;
325321

326322
// Apply options using extendState
@@ -636,6 +632,7 @@ export class Tempo {
636632
static create(options: t.Options = {}): typeof Tempo {
637633
const SandboxTempo = class extends (this as any) {
638634
static [Symbol.toStringTag] = 'TempoSandbox';
635+
static [$IsBase] = false;
639636
}
640637

641638
const discovery = options.discovery;
@@ -702,10 +699,13 @@ export class Tempo {
702699
setLogLevel(options.debug ?? Default?.debug ?? LOG.Info);
703700

704701
const rt = getRuntime();
705-
rt.state = undefined; // force fresh state
706-
const state = init(options);
702+
const isBase = !!this[$IsBase];
703+
if (isBase) rt.state = undefined; // force fresh state
704+
705+
const baseState = isBase ? undefined : Object.getPrototypeOf(this)[$Internal]();
706+
const state = init(options, isBase, baseState);
707707
(state as any)._count = 0;
708-
if (this[$IsBase]) {
708+
if (isBase) {
709709
_global = state;
710710
} else {
711711
ClassStates.set(this, state);

0 commit comments

Comments
 (0)