@ -1,42 +1,43 @@
# Format
`Format` es la API publica del artefacto `format` . Su responsabilidad es
centralizar todos los formatos que dependen de `locale` : numeros, moneda,
unidades y fechas. No traduce textos, no decide idioma y no depende de `langs` ;
solo consume una fuente de locale cuando se quiere reactivida d.
`Format` is the public API of the `format` artifact. Its responsibility is to
centralize every locale-dependent format: numbers, currency, units and dates. It
does not translate text, does not decide language and does not depend on `langs` ;
it only consumes a locale source when reactivity is wante d.
## Mapa del modulo
## Module map
| Pieza | Factory | Responsabilidad |
| ---------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `formats` | `createEngineFormat` , `createActiveFormat` | Agrega `numbers` , `currency` , `units` y `dates` bajo un mismo locale. |
| `numbers` | `createEngineNumbers` , `createActiveNumbers` | Formato y parseo numerico, separadores, porcentaj es, compact, currency/unit via `Intl.NumberFormat` . |
| `currency` | `createEngineCurrency` , `createActiveCurrency` | Moneda por region, formato de moneda, conversion y cache de rates. |
| `units` | `createEngineUnits` , `createActiveUnits` | Sistema metrico/imperial, unidades por defecto y conversiones. |
| `dates` | `createEngineDates` , `createActiveDates` | Orden de fecha, ciclo horario y formato fecha/hora. |
| Piece | Factory | Responsibility |
| ---------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `formats` | `createEngineFormat` , `createActiveFormat` | Agg regates `numbers` , `currency` , `units` and `dates` under one locale. |
| `numbers` | `createEngineNumbers` , `createActiveNumbers` | Number formatting and parsing, separators, percentag es, compact, currency/unit via `Intl.NumberFormat` . |
| `currency` | `createEngineCurrency` , `createActiveCurrency` | Currency by region, currency formatting, conversion and rate cache. |
| `units` | `createEngineUnits` , `createActiveUnits` | Metric/imperial system, default units and conversions. |
| `dates` | `createEngineDates` , `createActiveDates` | Date order, hour cycle and date/time formatting. |
Todos los submodulos siguen el mismo patro n:
Every submodule follows the same patter n:
- `Engine*` es puro y no depende de Svelte.
- `Active*` envuelve el engine con reactividad Svelte 5.
- `locale` puede ser string o funcion en engines.
- `localeSource` es la entrada reactiva en active wrappers.
- `dispose()` existe aunque hoy casi todo sea puro, para mantener el contrato comun.
- `Engine*` is pure and does not depend on Svelte.
- `Active*` wraps the engine with Svelte 5 reactivity.
- `locale` can be a string or a function in engines.
- `localeSource` is the reactive input in active wrappers.
- `dispose()` exists even though almost everything is pure today, to keep the
common contract.
## Uso rapido
## Quick use
```ts
import { createEngineFormat } from '$format';
const formats = createEngineFormat({ locale: 'es-ES' });
formats.numbers.format(1234.5); // "1234,5" segun Intl del runtime
formats.numbers.format(1234.5); // "1234,5" per the runtime's Intl
formats.currency.format(12.5); // "12,50 EUR"
formats.units.formatDefault(20, 'distance'); // "20 km"
formats.dates.formatDateTime(new Date());
```
En una app Svelte :
In a Svelte app :
```ts
import { createActiveFormat } from '$format';
@ -49,12 +50,12 @@ Format.setLocale('fr-FR');
Format.currency.format(99.5);
```
Bajo `createActiveApp(...)` normalmente no se crea a mano: `App.format` recibe
el `localeSource` de la app y se mueve con `App.prefs.locale` .
Under `createActiveApp(...)` it is usually not created by hand: `App.format`
receives the app's `localeSource` and moves with `App.prefs.locale` .
## LocaleSource
`Format` consume el alias compartido `LocaleSource` de `$locale` :
`Format` consumes the shared `LocaleSource` alias from `$locale` :
```ts
import type { LocaleSource } from '$locale';
@ -65,17 +66,17 @@ interface LocaleSource<L extends string = string> {
}
```
`createActiveFormat({ localeSource })` suscribe esa fuente y propaga los cambio s
a todos los submotores. Si el source no tiene `onLocaleChange` , el engine puede
leer `getLocale()` cuando se formatea, pero no recibe notificaciones activa s.
`createActiveFormat({ localeSource })` subscribes to that source and propagate s
changes to every submotor. If the source has no `onLocaleChange` , the engine can
read `getLocale()` when formatting, but it receives no active notification s.
## Locale por defecto
## Default locale
Cuando no se pasa `locale` ni `localeSource` , se usa
`DEFAULT_LOCALE = 'en-US'` . Esto evita depender del locale implicito del runtime,
que puede variar entre navegador, servidor, tests y CI.
When neither `locale` nor `localeSource` is passed, `DEFAULT_LOCALE = 'en-US'`
is used. This avoids depending on the runtime's implicit locale, which can vary
across browser, server, tests and CI.
Si quieres que el default de proyecto sea otro, pasalo en el composition root:
If you want a different project default, pass it in the composition root:
```ts
const App = createActiveApp({
@ -85,42 +86,42 @@ const App = createActiveApp({
});
```
## Regla Auto / Manual
## Auto / Manual rule
Todos los valores derivados siguen la misma dinamica :
Every derived value follows the same dynamic :
```ts
setX('auto'); // vuelve a derivar desde locale/Intl/framework
clearX(); // equivalente semantico de volver a auto
isXAuto(); // true si el usuario no fijo un valor manual
setX('auto'); // re-derive from locale/Intl/framework
clearX(); // semantic equivalent of going back to auto
isXAuto(); // true if the user did not fix a manual value
```
Un valor explicito siempre gana sobre el locale. Si el usuario fija `currency` ,
`system` , `hourCycle` , `dateOrder` o separadores numericos, cambiar el locale no
debe mover ese valor hasta que vuelva a `auto` .
An explicit value always wins over the locale. If the user fixes `currency` ,
`system` , `hourCycle` , `dateOrder` or numeric separators, changing the locale
must not move that value until it goes back to `auto` .
| Modulo | Valor derivado | Deriva de | Fijar | Volver a auto |
| ---------- | ------------------------ | --------------------- | -------------------------- | --------------------------------------------------------- |
| `currency` | moneda | region del locale | `setCurrency('EUR')` | `setCurrency('auto')` / `clearCurrency()` |
| `units` | sistema metrico/imperial | region del locale | `setSystem('metric')` | `setSystem('auto')` / `clearSystem()` |
| `dates` | `dateOrder` | `$libs/days` + locale | `setDateOrder('YMD')` | `setDateOrder('auto')` / `clearDateOrder()` |
| `dates` | `hourCycle` | `$libs/days` + locale | `setHourCycle(12)` | `setHourCycle('auto')` / `clearHourCycle()` |
| `numbers` | separadores y grouping | `Intl.NumberFormat` | `setDecimalSeparator(',')` | `setDecimalSeparator('auto')` / `clearDecimalSeparator()` |
| Module | Derived value | Derives from | Fix | Back to auto |
| ---------- | ----------------------- | --------------------- | -------------------------- | --------------------------------------------------------- |
| `currency` | currency | locale region | `setCurrency('EUR')` | `setCurrency('auto')` / `clearCurrency()` |
| `units` | metric/imperial system | locale region | `setSystem('metric')` | `setSystem('auto')` / `clearSystem()` |
| `dates` | `dateOrder` | `$libs/days` + locale | `setDateOrder('YMD')` | `setDateOrder('auto')` / `clearDateOrder()` |
| `dates` | `hourCycle` | `$libs/days` + locale | `setHourCycle(12)` | `setHourCycle('auto')` / `clearHourCycle()` |
| `numbers` | separators and grouping | `Intl.NumberFormat` | `setDecimalSeparator(',')` | `setDecimalSeparator('auto')` / `clearDecimalSeparator()` |
Ejemplo :
Example :
```ts
formats.currency.setCurrency('EUR');
formats.setLocale('en-US');
formats.currency.getCurrency(); // EUR: valor fijado por usuario
formats.currency.getCurrency(); // EUR: value fixed by the user
formats.currency.clearCurrency();
formats.currency.getCurrency(); // USD: vuelve a derivar del locale
formats.currency.getCurrency(); // USD: re-derives from the locale
```
## Numbers
`numbers` es la base para el resto cuando se inyecta en `currency` o `units` .
`numbers` is the base for the rest when injected into `currency` or `units` .
```ts
formats.numbers.format(1234567.89);
@ -134,27 +135,26 @@ formats.numbers.getDecimalSeparator();
formats.numbers.getGroupSeparator();
```
Los separadores pueden quedar en auto o ser preferencias de usuario :
Separators can stay auto or be user preferences :
```ts
formats.numbers.setDecimalSeparator(',');
formats.numbers.clearDecimalSeparator();
```
### Override de locale por llamada (Handoff 2026-05-25)
### Per-call locale override (Handoff 2026-05-25)
Igual que `dates` , los cinco metodos de `numbers` aceptan un argumento
opcional `locale` final que solo afecta a esa llamada. El engine sigue
siendo la fuente del cache, separadores, grouping y `defaultFormat` — el
override solo pivota el locale para esa entrada de
`getCachedNumberFormat` :
Like `dates` , the five `numbers` methods accept an optional trailing `locale`
argument that affects only that call. The engine stays the source of the cache,
separators, grouping and `defaultFormat` — the override only pivots the locale
for that entry of `getCachedNumberFormat` :
```ts
const nums = createEngineNumbers({ locale: 'es-ES' });
nums.format(1234.5); // '1234,5' (es-ES)
nums.format(1234.5, undefined, 'en-US'); // '1,234.5'
nums.getLocale(); // 'es-ES' (no muta)
nums.getLocale(); // 'es-ES' (does not mutate )
nums.formatCurrency(12.5, 'USD', undefined, 'en-US'); // '$12.50'
nums.formatPercent(0.5, undefined, 'en-US'); // '50%'
@ -162,25 +162,26 @@ nums.formatCompact(1_500_000, undefined, 'en-US'); // '1.5M'
nums.formatUnit(20, 'kilometer', undefined, 'en-US'); // '20 km'
```
Signature: `format*(value, options?, locale?)` . Si no se pasa `locale` ,
el motor usa su locale activo. Las preferencias de separadores siguen
viviendo en el engine — un override de locale no salta esas
preferencias, solo cambia la entrada del cache de `Intl.NumberFormat` .
Signature: `format*(value, options?, locale?)` . If no `locale` is passed, the
engine uses its active locale. The separator preferences still live in the
engine — a locale override does not skip those preferences, it only changes the
`Intl.NumberFormat` cache entry .
## Currency
`currency` resuelve moneda desde la region explicita del locale. No hace fallback
por idioma porque eso produce errores como tratar `es-AR` como `es-ES` .
`currency` resolves the currency from the locale's explicit region. It does not
fall back by language, because that produces errors such as treating `es-AR` as
`es-ES` .
```ts
formats.setLocale('es-AR');
formats.currency.getCurrency(); // ARS
formats.setLocale('es');
formats.currency.getCurrency(); // defaultCurrency, porque no hay region
formats.currency.getCurrency(); // defaultCurrency, because there is no region
```
Para conversiones se inyecta un provider de rates :
For conversions a rate provider is injected :
```ts
import { createRates } from '$format/currency';
@ -200,20 +201,20 @@ const formats = createEngineFormat({
await formats.currency.convert(10, 'USD');
```
`format` / `convert` usan la moneda **activa** ; sus variantes `As` toman la moneda
**explícita** (sin tocar la activa ):
`format` / `convert` use the **active** currency; their `As` variants take the
**explicit** currency (without touching the active one ):
```ts
formats.currency.formatAs(10, 'JPY'); // formatea 10 como JPY
await formats.currency.convertAs(10, 'USD', 'GBP'); // USD → GBP explícito
formats.currency.formatAs(10, 'JPY'); // formats 10 as JPY
await formats.currency.convertAs(10, 'USD', 'GBP'); // explicit USD → GBP
```
Si falta un provider o un rate, `currency` puede emitir diagnosticos por el
logger inyectado. Las categorias y mensajes viven en constantes del submodulo .
If a provider or a rate is missing, `currency` can emit diagnostics through the
injected logger. The categories and messages live in submodule constants .
## Units
`units` resuelve sistema por region y marca unidades por defecto e n
`units` resolves the system by region and marks default units i n
`units/unit-definitions.ts` .
```ts
@ -222,10 +223,10 @@ formats.units.getDefaultUnit('distance'); // kilometer | mile
formats.units.isDefaultUnit('mile', 'distance', 'imperial');
formats.units.formatDefault(20, 'distance');
formats.units.convert(1, 'mile', 'kilometer');
formats.units.convertToDefault(1, 'mile'); // → valor en la unidad por defecto del kind
formats.units.convertToDefault(1, 'mile'); // → value in the kind's default unit
```
El sistema tambien respeta auto/manual:
The system also respects auto/manual:
```ts
formats.units.setSystem('imperial');
@ -238,8 +239,8 @@ formats.units.getSystem(); // metric
## Dates
`dates` usa `$libs/days` como base pura y `formats/dates` como capa d e
engine/configuracion .
`dates` uses `$libs/days` as the pure base and `formats/dates` as th e
engine/configuration layer .
```ts
formats.dates.getDateOrder();
@ -252,93 +253,91 @@ formats.dates.formatRelative(3, 'day'); // "dentro de 3 días"
formats.dates.formatRelative(0, 'day'); // "hoy" (numeric:'auto')
```
`getHourCycle()` deriva del locale a traves de las utilidades de `days` ; si el
usuario fija `setHourCycle(12)` o `setHourCycle(24)` , esa preferencia queda
bloqueada hasta `clearHourCycle()` .
`getHourCycle()` derives from the locale through the `days` utilities; if the
user fixes `setHourCycle(12)` or `setHourCycle(24)` , that preference is locked
until `clearHourCycle()` .
`formatRelative(value, unit, options?, locale?)` envuelve
`Intl.RelativeTimeFormat` con cache compartido (`getCachedRelativeTimeFormat`
en `$libs/days` ). El caller pasa magnitud (`-1` past, `+3` future) y unidad
`formatRelative(value, unit, options?, locale?)` wraps `Intl.RelativeTimeFormat`
with a shared cache (`getCachedRelativeTimeFormat` in `$libs/days` ). The caller
passes a magnitude (`-1` past, `+3` future) and a unit
(`'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'`).
Defaults: `numeric: 'auto'` (colapsa 0/-1/+1 en palabra s, "yesterday"/"today"),
Defaults: `numeric: 'auto'` (collapses 0/-1/+1 into word s, "yesterday"/"today"),
`style: 'long'` .
### Override de locale por llamada (Handoff 2026-05-25)
### Per-call locale override (Handoff 2026-05-25)
`formatDate / formatTime / formatDateTime` aceptan un tercer argumento
opcional `locale` que **solo afecta a esa llamada** . El engine sigu e
siendo la fuente de cache, `hourCycle` , y conflict handling — el overrid e
solo pivota el locale para esa entrada de `getCachedDateFormat` :
`formatDate / formatTime / formatDateTime` accept an optional third `locale`
argument that **affects only that call** . The engine stays the source of th e
cache, `hourCycle` , and conflict handling — the override only pivots the local e
for that entry of `getCachedDateFormat` :
```ts
const dates = createEngineDates({ locale: 'es-ES' });
dates.formatDate(new Date()); // 25 may 2026 (es-ES)
dates.formatDate(new Date(), undefined, 'en-US'); // May 25, 2026
dates.getLocale(); // 'es-ES' (no muta)
dates.getLocale(); // 'es-ES' (does not mutate )
```
Regla s:
Rul es:
- El cache (`getCachedDateFormat`) sigue funcionando — keyea por
`(locale × options)` , así que cada override es un slot propio .
- `resolveCallHourCycle(locale)` : cuando hay override y la preferencia
esta en auto, el ciclo horario se resuelve desde el locale OVERRIDE
(en-US → 12h) en vez de forzar el del engine (es-ES → 24h).
- Si el usuario fijo `setHourCycle(...)` explicitamente, esa preferencia
gana sobre cualquier locale.
- The cache (`getCachedDateFormat`) keeps working — it keys by
`(locale × options)` , so each override is its own slot .
- `resolveCallHourCycle(locale)` : when there is an override and the preference
is on auto, the hour cycle is resolved from the OVERRIDE locale (en-US → 12h)
instead of forcing the engine's (es-ES → 24h).
- If the user fixed `setHourCycle(...)` explicitly, that preference wins over any
locale.
### Conflict handling preset / per-axis (Handoff 2026-05-25)
`Intl.DateTimeFormat` rechaza la mezcla de `dateStyle` / `timeStyle`
(presets) con cualquier per-axis field (`weekday`, `era` , `year` ,
`month` , `day` , `dayPeriod` , `hour` , `minute` , `second` ,
`fractionalSecondDigits` ) o con `timeZoneName` . Si el caller mezcla
ambos, Intl lanza `TypeError: Invalid option : option` .
`Intl.DateTimeFormat` rejects mixing `dateStyle` / `timeStyle` (presets) with any
per-axis field (`weekday`, `era` , `year` , `month` , `day` , `dayPeriod` , `hour` ,
`minute` , `second` , `fractionalSecondDigits` ) or with `timeZoneName` . If the
caller mixes both, Intl throws `TypeError: Invalid option : option` .
El engine drop-ea los presets por defecto cuando detecta cualquier
campo conflictivo:
The engine drops the presets by default when it detects any conflicting field:
```ts
// Default OK — solo dateStyle: 'medium' inyectado
// Default OK — only dateStyle: 'medium' injected
dates.formatDate(sample); // '25 may 2026'
// Per-axis presente → engine drop-ea dateStyle: 'medium'
// Per-axis present → engine drops dateStyle: 'medium'
dates.formatDate(sample, { weekday: 'long', day: 'numeric', month: 'long' });
// 'lunes, 25 de mayo'
// timeZoneName presente → engine drop-ea timeStyle: 'short'
// timeZoneName present → engine drops timeStyle: 'short'
dates.formatTime(sample, { timeZoneName: 'short', timeZone: 'UTC' });
```
Aplica a las tres funcione s (`formatDate`, `formatTime` ,
`formatDateTime` ); cualquier per-axis o `timeZoneName` desactiva
**ambos** presets (`dateStyle` y `timeStyle` ) porque la regla de Intl e s
estricta — un solo per-axis hace ilegales ambos .
It applies to the three function s (`formatDate`, `formatTime` ,
`formatDateTime` ); any per-axis or `timeZoneName` disables **both** presets
(`dateStyle` and `timeStyle` ) because Intl's rule is strict — a single per-axi s
makes both illegal .
### Precedencia de hourCycle (Handoff 2026-05-25)
### hourCycle precedenc e (Handoff 2026-05-25)
`withHourCycle` aplica la preferencia del engine **solo cuando el caller
no ha pasado `hourCycle` ni `hour12` **. Cualquier valor del caller gana :
`withHourCycle` applies the engine's preference **only when the caller passed
neither `hourCycle` nor `hour12` **. Any caller value wins :
```ts
const dates = createEngineDates({ locale: 'es-ES' }); // pref auto = 24h
dates.formatTime(sample); // '14:30' (24h por locale)
dates.formatTime(sample, { hourCycle: 'h12' }); // '2:30 p. m.' (caller gana )
dates.formatTime(sample); // '14:30' (24h from locale)
dates.formatTime(sample, { hourCycle: 'h12' }); // '2:30 p. m.' (caller wins )
dates.setHourCycle(12); // fija preferencia 12h
dates.setHourCycle(12); // fixes the 12h preference
dates.formatTime(sample); // '2:30 p. m.'
dates.formatTime(sample, { hour12: false }); // '14:30' (caller gana )
dates.formatTime(sample, { hour12: false }); // '14:30' (caller wins )
```
Sin esta regla, un `<FormatDate hourCycle="h12" />` se convertía silenciosamente
en `h23` cuando el locale resolvía a 24h.
Without this rule, a `<FormatDate hourCycle="h12" />` was silently turned into
`h23` when the locale resolved to 24h.
## Active listeners
Los wrappers activos exponen listeners para que una UI o un modulo superior
pueda reaccionar sin inspeccionar internals:
Active wrappers expose listeners so a UI or a higher-level module can react
without inspecting internals:
```ts
const offLocale = Format.numbers.onLocaleChange((locale) => {
@ -354,12 +353,12 @@ const offCurrency = Format.currency.onCurrencyChange((currency) => {
});
```
`dispose()` limpia estas suscripcione s.
`dispose()` clears these subscription s.
## Integracion recomendada
## Recommended integration
En `active-app` , `Format` lee el locale regional desde `core.prefs.locale`
cuando la dimension existe :
In `active-app` , `Format` reads the regional locale from `core.prefs.locale`
when the dimension exists :
```ts
const App = createActiveApp({
@ -376,7 +375,7 @@ App.prefs.locale.set('ar-EG');
App.format.dates.formatDate(new Date());
```
Si un modulo necesita solo formatos sin toda la app, usa el engine aislado :
If a module needs only formats without the whole app, use the standalone engine :
```ts
const formats = createEngineFormat({
@ -386,8 +385,8 @@ const formats = createEngineFormat({
## Testing
Tests utile s:
Useful test s:
- `npx vitest run src/arts/format`
- la documentacion interactiva de `format` dentro de `/active` .
- `/active` /`/uix` para validar `Prefs + langs + Format + Dom` juntos .
- the interactive `format` documentation insi de `/active` .
- `/active` / `/uix` to validate `Prefs + langs + Format + Dom` together .