Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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