Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tempo-monorepo",
"version": "3.0.3",
"version": "3.1.0",
"private": true,
"engines": {
"node": ">=20.0.0"
Expand Down Expand Up @@ -59,4 +59,4 @@
"edgedriver@6.3.0": true,
"geckodriver@6.1.0": true
}
}
}
2 changes: 1 addition & 1 deletion packages/library/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@magmacomputing/library",
"version": "3.0.3",
"version": "3.1.0",
"description": "Shared utility library for Tempo",
"author": "Magma Computing Solutions",
"license": "MIT",
Expand Down
21 changes: 16 additions & 5 deletions packages/library/src/common/international.library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ const getLF = memoizeFunction((locale?: string, type: Intl.ListFormatType = 'con
});

/** memoized helper for Intl.DateTimeFormat instances */
const getDTF = memoizeFunction((locale?: string) => {
return new Intl.DateTimeFormat(locale);
export const getDTF = memoizeFunction((locale?: string, options?: Intl.DateTimeFormatOptions) => {
return new Intl.DateTimeFormat(locale, options);
});

/** memoized helper for Intl.NumberFormat instances */
Expand Down Expand Up @@ -43,12 +43,13 @@ export function getDateTimeFormat() {
return getDTF().resolvedOptions();
}

/** return the canonicalized locale string */
export function canonicalLocale(locale: string) {
/** return the canonicalized locale string, or undefined if invalid */
export function canonicalLocale(locale: string): string | undefined {
try {
return Intl.getCanonicalLocales(locale.replace(/_/g, '-'))[0];
} catch (e) {
return locale;
console.warn(`[Tempo] dropping invalid locale: '${locale}'`, e);
return undefined;
}
}

Expand Down Expand Up @@ -84,6 +85,16 @@ export function formatNumber(value: number, locale?: string, options?: Intl.Numb
}
}

/** return a localized day period string (e.g., 'AM', 'PM', 'de la mañana') */
export function formatDayPeriod(value: number, locale?: string, options?: Intl.DateTimeFormatOptions) {
try {
const parts = getDTF(locale, options).formatToParts(value);
return parts.find(p => p.type === 'dayPeriod')?.value;
} catch (e) {
return undefined;
}
}

/** return a localized unit string (e.g., '2 days') */
export function formatUnit(value: number, unit: string, locale?: string, unitDisplay: Intl.NumberFormatOptions['unitDisplay'] = 'long') {
try {
Expand Down
15 changes: 12 additions & 3 deletions packages/library/src/common/string.library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,32 @@ import { isString, isObject, isNumeric, assertCondition, assertString } from '#l
*/
export function trimAll(str: string | number, pat?: RegExp) {
return str
.toString() // coerce to String
.toString() // coerce to String
.replace(pat!, '') // remove regexp, if supplied
.replace(/\t/g, ' ') // replace <tab> with <space>
.replace(/(\r\n|\n|\r)/g, ' ') // replace <return> & <newline>
.replace(/\s{2,}/g, ' ') // trim multiple <space>
.trim() // leading/trailing <space>
.trim() // leading/trailing <space>
}

/** every word has its first letter capitalized */
export function toProperCase<T extends string>(...str: T[]) {
return str
.flat() // in case {str} was already an array
.flat() // in case {str} was already an array
.map(text => text.replace(/\w\S*/g,
word => word.charAt(0).toUpperCase() + word.substring(1).toLowerCase()))
.join(' ') as T
}

/** only the first letter of the entire string is capitalized (locale-aware) */
export function toTitleCase(str: string, locale?: string): string {
try {
return str.charAt(0).toLocaleUpperCase(locale) + str.slice(1).toLocaleLowerCase(locale);
} catch {
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const PAT = /[A-Z\xC0-\xD6\xD8-\xDE]?[a-z\xDF-\xF6\xF8-\xFF]+|[A-Z\xC0-\xD6\xD8-\xDE]+(?![a-z\xDF-\xF6\xF8-\xFF])|\d+/g;
export const toCamelCase = <T extends string>(sentence: T) => {
Expand Down
1 change: 1 addition & 0 deletions packages/tempo/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export default defineConfig({
text: 'Core Concepts',
items: [
{ text: 'Configuration', link: '/doc/tempo.config' },
{ text: 'Registries', link: '/doc/tempo.registry' },
{ text: 'Smart Parsing', link: '/doc/tempo.parse' },
{ text: 'Parse Planner', link: '/doc/tempo.planner' },
{ text: 'Regional Parsing (MDY)', link: '/doc/tempo.month-day' },
Expand Down
22 changes: 22 additions & 0 deletions packages/tempo/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,32 @@

# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [3.1.0] - 2026-06-13

### Added
- `registry: { formats, locales }` namespaces to `Tempo.init()` and `Tempo` instance configurations.
- `format: { localize }` namespace to options for toggling localizing formats.
- `Tempo.registry` static accessor for retrieving the active registry configurations.
- **Chained Formatting Modifiers**: Introduced a powerful format modifier engine (e.g. `{mon:locale:upper}`) allowing dynamic casing (`:upper`, `:lower`, `:title`), ordinal suffixes (`:ord`), and deep localization (`:locale`) directly within template strings.
- **Global LOCALE Registry**: Implemented a global `STATE.LOCALE` registry (`Tempo.init({ locale: 'fr-FR', registry: { locales: { ... } } })`) to provide centralized, context-aware translations.
- **Auto-Localization**: Added a `format: { localize: true }` global configuration flag that automatically applies the `:locale` modifier to all formatting tokens behind the scenes.
- **Internationalized Parsing**: Implemented robust localized parsing capabilities natively into Tempo. By opting in via `Tempo.init({ locale: 'fr-FR', parse: { localize: true } })`, Tempo automatically registers localized variants for months, weekdays, and relative terms (yesterday, today, tomorrow) based on your configured `locale`. This engine dynamically strips accents (so "février" and "fevrier" both match safely), ensuring resilient fuzzy-matching for user input out-of-the-box.

### Deprecated
- Top-level `formats` configuration key. Use `registry: { formats: ... }` instead.
- `Tempo.formats` static accessor. Use `Tempo.registry.formats` instead.


### Changed
- **Optimized Intl Instantiation**: Refactored `Intl.DateTimeFormat` and `Intl.RelativeTimeFormat` creation using memoized helpers (`getDTF`, `getRTF`) across the formatting engine, drastically reducing object instantiation overhead during rapid format evaluations.
- **Term Localization Precedence**: Upgraded the Term formatting resolution pipeline to support falling back across a strict precedence: Global Registry > Plugin Bundled Dictionary > term's existing label/value.
- **Plugin Localization Capabilities**: Refactored the `TimeOfDay` plugin (and underlying range resolution logic) to natively bundle and evaluate custom `locale` objects across languages without requiring external overrides in order to demonstrate the new `parse: { localize: true }` capability.

## [3.0.2] - 2026-06-12

### Security
Expand Down
41 changes: 0 additions & 41 deletions packages/tempo/doc/release-notes-v3.0.0.md

This file was deleted.

3 changes: 2 additions & 1 deletion packages/tempo/doc/releases/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

Explore the evolution of Tempo through its version history.

- [Version 3.x (Planned)](./v3.x) - Removal of deprecated shorthands and major engine hardening.
- [Version 3.x](./v3.x) - Removal of deprecated shorthands and major engine hardening.
- [Version 4.x (Planned)](./v4.x) - Removal of deprecated legacy discovery root properties.
- [Version 2.x (Current)](./v2.x) - Modular architecture, Shorthand engine, and Ticker stability.
- [Version 1.x (Legacy)](./v1.x) - Initial public release and Temporal polyfill integration.
- [Version 0.x (Legacy)](./v0.x) - Initial release.
Expand Down
34 changes: 33 additions & 1 deletion packages/tempo/doc/releases/v3.x.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,45 @@
# 📜 Version 3.x History

## [v3.0.0] - (Planned)
## [v3.1.0] - 2026-06-13

### ✨ What's New
- **Chained Formatting Modifiers**: A powerful new format modifier engine (`{mon:locale:upper}`) allowing dynamic casing (`:upper`, `:lower`), ordinal suffixes (`:ord`), and deep localization (`:locale`) dynamically via the native `Intl` API.
- **Auto-Localization Engine**: Global configurations for `format: { localize: true }` and `parse: { localize: true }` provide a massive leap forward in out-of-the-box internationalization. Tempo can now intelligently parse localized input (months, weekdays, relative terms like 'demain') and automatically format localized output using memoized, high-performance `Intl` strategies.
- **Global `locales` Registry**: Centralized management for augmenting specific term strings per locale globally across instances via `Tempo.init({ locale: 'fr-FR', registry: { locales: { ... } } })`.

### 🏗️ Internal Refactoring
- **Intl Instantiation**: Upgraded internal architectures to memoize and pool `Intl.DateTimeFormat` objects seamlessly, ensuring parsing localization generation and output formatting impose virtually zero performance hit on hot execution paths.
- **Term Localization**: Upgraded the Term formatting resolution pipeline to support falling back across a strict precedence: Global Registry > Plugin Bundled Dictionary > term's existing label/value.

## [v3.0.0] - 2026-06-08

### 🚨 Major Breaking Changes
- **Term Registry Consolidation**: Removed the legacy and deprecated `term` property from the `Discovery` configuration object. All Term-based plugins must now be registered via the `terms` (plural) array.
- **Shorthand Configuration Removal**: Removed support for shorthand root-level properties in the `Discovery` object that have been superseded by nested configuration groups:
- `relativeTime` shorthand has been removed; use `intl.relativeTime` instead.
- `term` shorthand has been removed; use `terms` instead.
- **Strict Parsing Mode**: The parser now enforces a stricter `guard` check by default, reducing the likelihood of "false positive" matches on ambiguous strings.
- **Ticker Module Extraction**: To lighten the core bundle, the `TickerModule` has been extracted into its own standalone premium plugin (`@magmacomputing/tempo-plugin-ticker`). It is protected by a License Key via the Tempo Registry.

### ✨ What's New
- **Formatting Module Additions**: Added new compact date tokens (`{dmy}`, `{mdy}`, `{ymd}`) for generating 8-digit compact date strings (e.g. `24102026`). `{hhmiss}` has been renamed to `{hms}` for consistency.
- **Ordinal Tokens**: Uppercase variants of standard date tokens (`{DAY}`, `{WW}`, `{MM}`) now output their ordinal string representation (e.g., `24th`, `1st`, `2nd`).

### 📦 Migration Path for `Tempo.ticker()` Users
If you are upgrading from v2.x and your application relies on `Tempo.ticker()`, you will need to update your integration:
1. **Install the Plugin**: `npm install @magmacomputing/tempo-plugin-ticker`
2. **Activate your License**: Visit [registry.magmacomputing.com.au](https://registry.magmacomputing.com.au) to obtain your free JWT license key.
3. **Register the Plugin**: Wire the key into your application and extend Tempo:
```javascript
import { Tempo } from '@magmacomputing/tempo';
import { TickerModule } from '@magmacomputing/tempo-plugin-ticker';

Tempo.init({ license: 'YOUR_JWT_KEY' });
Tempo.extend(TickerModule);
```

### 🏗️ Internal Refactoring
- **Zero-Fallback Initialization**: Cleaned up the `Tempo.init()` bootstrap logic to remove legacy compatibility layers, resulting in a cleaner internal state and reduced bundle size.
- **Build Pipelines**: Fully synchronized build pipelines and TS declarations to ensure `vitest` and `tsc` operate seamlessly across local and premium workspaces.
- **ISO Getter Precision**: Upgraded the `.iso` property getter from native `Date.toISOString()` to Temporal's `Instant.toString()`. This provides full ISO 8601 nanosecond precision and conforms to RFC 3339 by gracefully omitting fractional seconds when they evaluate to exactly zero.

21 changes: 21 additions & 0 deletions packages/tempo/doc/releases/v4.x.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# 📜 Version 4.x History

## [v4.0.0] - (Planned)

### 🚨 Major Breaking Changes
- **Configuration Namespace Enforcement**: Removed all legacy root-level property access that was deprecated during the `v3.x` lifecycle.
- `formats` configuration key has been entirely removed from the `Options` and `Discovery` interfaces. You must use `registry: { formats: ... }` instead.

### 🗑️ API Removals
- **Removed Static Accessors**:
- `Tempo.formats` has been completely removed. Use `Tempo.registry.formats` instead.

### 🏗️ Internal Architecture
- **Namespace-Only Configurations**: The internal `Config` state mapping now exclusively enforces nested schema access without mapping wrappers.
- **Registry Consolidation**: The overarching architectural goal for v4.x is to move *all* remaining data dictionaries into the `registry` namespace to fully separate data stores from module settings. The following top-level options are slated to be transitioned under `registry` in a phased approach:
- `event` -> `registry.events`
- `period` -> `registry.periods`
- `snippet` -> `registry.snippets`
- `layout` -> `registry.layouts`
- `timeZones` -> `registry.timeZones`
- `numbers` -> `registry.numbers`
Loading
Loading