@ -1,30 +1,71 @@
# Formats
`Formats` es la API publica del artefacto `fmts` , que agrupa formatos
localizados de Active.
`Formats` es la API publica del artefacto `fmts` . 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 `Lang` ;
solo consume una fuente de locale cuando se quiere reactividad.
Agrupa los subdominios que dependen de locale:
El objetivo es que una app tenga una sola verdad :
- `numbers` : numeros, parseo, separadores, percent, compact, currency y unit via `Intl.NumberFormat` .
- `currency` : moneda por region, formato de moneda y cache de factores de conversion.
- `units` : unidades, sistema metrico/imperial, conversiones y unidades por defecto marcadas.
- `dates` : orden de fecha, ciclo horario y formato de fecha/hora.
```ts
App.setLocale('es-AR');
App.Formats.numbers.format(1234.5);
App.Formats.currency.getCurrency(); // ARS
App.Formats.units.getSystem(); // metric
App.Formats.dates.getDateOrder();
```
## Mapa del modulo
| Pieza | Factory | Responsabilidad |
| --- | --- | --- |
| `formats` | `createEngineFormats` , `createActiveFormats` | Agrega `numbers` , `currency` , `units` y `dates` bajo un mismo locale. |
| `numbers` | `createEngineNumbers` , `createActiveNumbers` | Formato y parseo numerico, separadores, porcentajes, 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. |
Todos los submodulos siguen el mismo patron:
- `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.
## Uso rapido
```ts
import { createEngineFormats } from '$fmts';
const formats = createEngineFormats({ locale: 'es-ES' });
formats.numbers.format(1234.5);
formats.currency.format(12.5);
formats.units.formatDefault(20, 'distance');
formats.numbers.format(1234.5); // "1234,5" segun Intl del runtime
formats.currency.format(12.5); // "12,50 EUR"
formats.units.formatDefault(20, 'distance'); // "20 km"
formats.dates.formatDateTime(new Date());
```
## Locale
En una app Svelte:
`Formats` no depende de `lang` . Para integracion reactiva acepta una fuente
minima — el alias compartido `LocaleSource` de `$locale` :
```ts
import { createActiveFormats } from '$fmts';
const Formats = createActiveFormats({
locale: 'en-US'
});
Formats.setLocale('fr-FR');
Formats.currency.format(99.5);
```
Bajo `createActiveApp(...)` normalmente no se crea a mano: `App.Formats` recibe
el `localeSource` de la app y se mueve con `App.setLocale(...)` .
## LocaleSource
`Formats` consume el alias compartido `LocaleSource` de `$locale` :
```ts
import type { LocaleSource } from '$locale';
@ -35,45 +76,44 @@ interface LocaleSource<L extends string = string> {
}
```
`createActiveFormats({ localeSource })` consume esa fuente y sincroniza todos
los submotores. Cuando se compone bajo `createActiveApp(...)` , el
`localeSource` lo proporciona automaticamente `App.Lang` , asi que un solo
`App.setLocale(...)` mueve `numbers` , `currency` , `units` y `dates` en bloque.
`createActiveFormats({ localeSource })` suscribe esa fuente y propaga los cambios
a todos los submotores. Si el source no tiene `onLocaleChange` , el engine puede
leer `getLocale()` cuando se formatea, pero no recibe notificaciones activas.
### Locale por defecto
## Locale por defecto
Cuando no se pasa `locale` ni `localeSource` , `createEngineFormats` aplica
`DEFAULT_LOCALE = 'en-US'` . Esto evita pasar cadena vacia a `Intl.*` (que cae
silenciosamente al locale del runtime y genera comportamiento opaco entre
entornos).
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.
## Lifecycle
Si quieres que el default de proyecto sea otro, pasalo en el composition root:
`EngineFormats` y `ActiveFormats` exponen `dispose()` . La capa engine delega
en cada sub-motor (`numbers`, `currency` , `units` , `dates` ) reflectivamente —
hoy son puros, pero `currency` puede recibir un `RateFetcher` con timers en
el futuro y la limpieza queda preparada sin tocar consumidores.
```ts
const App = createActiveApp({
formats: { locale: 'es-ES' }
});
```
## Valores Auto
## Regla Auto / Manual
Todos los valores derivados siguen la misma dinamica:
```ts
setX('auto'); // vuelve a derivar desde locale/Intl/framework
clearX(); // equivalente semantico de volver a auto
isXAuto(); // true si el valor no esta fijado por el usuario
isXAuto(); // true si el usuario no fijo un valor manual
```
Un valor explicito siempre gana sobre el locale. Si el usuario fija
`currency` , `system` , `hourCycle ` o separadores numericos, cambiar el locale no
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` .
| 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` | locale | `setDateOrder('YMD')` | `setDateOrder('auto')` / `clearDateOrder()` |
| `dates` | `hourCycle` | `Intl.DateTimeFormat` | `setHourCycle(12)` | `setHourCycle('auto')` / `clearHourCycle()` |
| `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()` |
Ejemplo:
@ -87,32 +127,156 @@ formats.currency.clearCurrency();
formats.currency.getCurrency(); // USD: vuelve a derivar del locale
```
## Unidades Por Defecto
## Numbers
`numbers` es la base para el resto cuando se inyecta en `currency` o `units` .
```ts
formats.numbers.format(1234567.89);
formats.numbers.formatPercent(0.42);
formats.numbers.formatCompact(1200000);
formats.numbers.formatCurrency(99.5, 'EUR');
formats.numbers.formatUnit(20, 'kilometer');
formats.numbers.parse('1.234,50');
formats.numbers.getDecimalSeparator();
formats.numbers.getGroupSeparator();
```
Los separadores pueden quedar en auto o ser preferencias de usuario:
```ts
formats.numbers.setDecimalSeparator(',');
formats.numbers.clearDecimalSeparator();
```
## 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` .
```ts
formats.setLocale('es-AR');
formats.currency.getCurrency(); // ARS
formats.setLocale('es');
formats.currency.getCurrency(); // defaultCurrency, porque no hay region
```
Para conversiones se inyecta un provider de rates:
```ts
import { createRates } from '$fmts/curr';
const rates = createRates({
initial: {
base: 'EUR',
rates: { USD: 1.08, GBP: 0.86 }
}
});
const formats = createEngineFormats({
locale: 'es-ES',
currency: { rates }
});
Las unidades por defecto estan marcadas en `unts/unit-definitions.ts` mediante
`defaultFor` . Por ejemplo:
await formats.currency.convert(10, 'USD');
```
- `metric:distance` -> `kilometer`
- `imperial:distance` -> `mile`
- `metric:temperature` -> `celsius`
- `imperial:temperature` -> `fahrenheit`
Si falta un provider o un rate, `currency` puede emitir diagnosticos por el
logger inyectado. Las categorias y mensajes viven en constantes del submodulo.
La API publica para leerlas es:
## Units
`units` resuelve sistema por region y marca unidades por defecto en
`unts/unit-definitions.ts` .
```ts
formats.units.getDefaultUnit('distance');
formats.units.getSystem(); // metric | imperial
formats.units.getDefaultUnit('distance'); // kilometer | mile
formats.units.isDefaultUnit('mile', 'distance', 'imperial');
formats.units.formatDefault(20, 'distance');
formats.units.convert(1, 'mile', 'kilometer');
```
El sistema tambien respeta auto/manual:
```ts
formats.units.setSystem('imperial');
formats.setLocale('es-ES');
formats.units.getSystem(); // imperial
formats.units.clearSystem();
formats.units.getSystem(); // metric
```
## Fechas
## Date s
Los defaults de fecha/hora pertenecen tambien a `Formats` :
`dates` usa `$libs/days` como base pura y `fmts/dates` como capa de
engine/configuracion.
```ts
formats.dates.getDateOrder();
formats.dates.getHourCycle();
formats.dates.formatDate(new Date());
formats.dates.formatTime(new Date());
formats.dates.formatDateTime(new Date());
```
La implementacion usa `$libs/days` como base pura y `fmts/dates` como capa de
engine/configuracion.
`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()` .
## Active listeners
Los wrappers activos exponen listeners para que una UI o un modulo superior
pueda reaccionar sin inspeccionar internals:
```ts
const offLocale = Formats.numbers.onLocaleChange((locale) => {
console.log('locale changed', locale);
});
const offPrefs = Formats.dates.onPreferenceChange(() => {
console.log('date preferences changed');
});
const offCurrency = Formats.currency.onCurrencyChange((currency) => {
console.log('currency changed', currency);
});
```
`dispose()` limpia estas suscripciones.
## Integracion recomendada
En `aapp` , `Formats` debe compartir locale con `Lang` y `Frontend` :
```ts
const App = createActiveApp({
lang: { locale: 'es-ES', schema },
formats: {
currency: { defaultCurrency: 'EUR' }
}
});
App.setLocale('ar');
App.Formats.dates.formatDate(new Date());
```
Si un modulo necesita solo formatos sin toda la app, usa el engine aislado:
```ts
const formats = createEngineFormats({
locale: () => requestLocale
});
```
## Testing
Tests utiles:
- `npx vitest run src/arts/fmts`
- `/test/fmts` para validar locale compartido, auto/manual y UI.
- `/test/ecosystem` para validar `Lang + Formats + Frontend + Dom` juntos.