From 2415f4942f75ae91cd2e5e116ec1a7bfcd08ff14 Mon Sep 17 00:00:00 2001 From: dev Date: Wed, 29 Apr 2026 13:50:51 +0200 Subject: [PATCH] Expand formats documentation --- NEXT_STEPS.md | 1 + src/arts/fmts/README.md | 266 ++++++++++++++++++++++++++++++++-------- 2 files changed, 216 insertions(+), 51 deletions(-) diff --git a/NEXT_STEPS.md b/NEXT_STEPS.md index 556a729..1056d52 100644 --- a/NEXT_STEPS.md +++ b/NEXT_STEPS.md @@ -18,6 +18,7 @@ Estado al cierre: - `src/arts/aapp/test/ecosystem.integration.test.ts` ampliado para cubrir rol `viewer` no-allow y cache re-scoped por locale. - Referencias residuales de marca anterior eliminadas de `src/` fuera de rutas temporales: docs, páginas de test y constantes de cookies/headers auth usan ahora `Active/active`. - `src/arts/conn/README.md` ampliado: contrato de raíz/conexión/canal, estados, transportes, request/reply, reconnect, heartbeat, sesión, diagnostics/logger, errores y testing. +- `src/arts/fmts/README.md` ampliado con guia de uso, `LocaleSource`, contrato auto/manual, submodulos, listeners, integracion con `aapp` y tests. - `fmts` redujo boilerplate activo con helpers `readFrom` / `writeTo` y los engines comparten directamente las funciones de `createFormatsLocaleState`; APIs públicas sin cambios. - No tocar `src/web/routes/temp/` hasta decidir que hacer con esa pagina. - No commitear `.idea/`, `.claude/` ni `.opencode/`. diff --git a/src/arts/fmts/README.md b/src/arts/fmts/README.md index e2da6bd..4359f4d 100644 --- a/src/arts/fmts/README.md +++ b/src/arts/fmts/README.md @@ -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,46 +76,45 @@ interface LocaleSource { } ``` -`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()` | -| `numbers` | separadores y grouping | `Intl.NumberFormat` | `setDecimalSeparator(',')` | `setDecimalSeparator('auto')` / `clearDecimalSeparator()` | +| 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()` | Ejemplo: @@ -87,32 +127,156 @@ formats.currency.clearCurrency(); formats.currency.getCurrency(); // USD: vuelve a derivar del locale ``` -## Unidades Por Defecto +## Numbers -Las unidades por defecto estan marcadas en `unts/unit-definitions.ts` mediante -`defaultFor`. Por ejemplo: +`numbers` es la base para el resto cuando se inyecta en `currency` o `units`. -- `metric:distance` -> `kilometer` -- `imperial:distance` -> `mile` -- `metric:temperature` -> `celsius` -- `imperial:temperature` -> `fahrenheit` +```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(); +``` -La API publica para leerlas es: +Los separadores pueden quedar en auto o ser preferencias de usuario: ```ts -formats.units.getDefaultUnit('distance'); +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 } +}); + +await formats.currency.convert(10, 'USD'); +``` + +Si falta un provider o un rate, `currency` puede emitir diagnosticos por el +logger inyectado. Las categorias y mensajes viven en constantes del submodulo. + +## Units + +`units` resuelve sistema por region y marca unidades por defecto en +`unts/unit-definitions.ts`. + +```ts +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'); ``` -## Fechas +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 +``` -Los defaults de fecha/hora pertenecen tambien a `Formats`: +## Dates + +`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. +