phase-0: foundation, both apps boot end to end

Monorepo (pnpm workspaces) with two deployable apps and three pure packages.

apps/api (Hono on Node): Zod validated env that fails fast and names the problem,
Kysely factories for SQLite and Postgres chosen by DATABASE_URL scheme, portable
migrations covering the whole SPEC section 5 schema, Better Auth with the four
roles and seeded demo accounts, localized error envelope, /healthz and /readyz,
graceful SIGTERM drain. Dialect specific SQL is confined to the two factories.

apps/web (Next.js App Router): locale routed shell in es and en with a language
switcher, sign in screen, and a runtime /api proxy so the browser only ever sees
one origin and cookies stay first party.

packages/i18n ships both catalogs complete; es is generated from COPY.md and a
test re-derives it from the document on every run so it cannot drift.
packages/contracts holds the Zod schemas and the typed client the web app uses.

Verified: 43 vitest tests, 14 Playwright tests on mobile and desktop, typecheck
and lint clean, migrate and seed from a clean database, sign in through the proxy
with CSRF rejection of foreign origins.

Not verified here: docker compose. This user has no access to the docker socket.

RULES.md is absent from docs/, so packages/rules exports only RULES_VERSION and
no tax rule, check digit or deadline was invented. See DECISIONS.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Michilis
2026-09-03 21:46:35 +00:00
co-authored by Claude Opus 5
commit ae2ea20b7e
106 changed files with 11541 additions and 0 deletions
+254
View File
@@ -0,0 +1,254 @@
# COPY.md: UI Copy (es, Paraguayan voseo)
Authoritative copy source. The agent transfers these verbatim into the `es` catalog of `packages/i18n` (structure mirrors the sections below) and must not rewrite them. Strings not listed here follow the tone rules in section 0 and get added to the same catalog.
**Multi-language policy (day 1):** the `en` catalog ships COMPLETE alongside `es`, same keys (type-checked parity). The agent writes the English itself following section 0-EN: natural English, not literal translation. Both locales are first-class: language switcher in the header and profile, locale routing, and the API localizes its user-facing output (errors, notifications, emails) per the user's stored locale. Official form previews and PDFs stay Spanish in every locale (they mirror DNIT forms); the UI around them localizes. New locales are one catalog file.
## 0-EN. English tone rules
- Audience: expats and international users in Paraguay. Friendly, direct, second person.
- Keep Paraguayan tax terms in Spanish with a short gloss on first use per screen: "factura (invoice)", "vencimiento (due date)", RUC, IVA, IRP, Formulario 120/515 stay as-is.
- Same restraint rules as Spanish: no exclamation stacking, only ✓ and 🔥, no em dashes, money always `Gs. 1.234.567`.
- Example anchors (match this register): landing hero = "Your taxes, on autopilot." / subtitle = "Scan your facturas and we build your books, your deductions and your declarations. Never miss a vencimiento again." / bandeja empty = "Inbox clear ✓".
## 0. Tone rules
- Voseo always: "ingresá", "revisá", "tenés", "podés". Never "ingrese/usted", never "tú".
- Plain words, no tax jargon on primary surfaces. Jargon allowed inside "Ver detalle" layers and the form preview.
- Money always via formatGs: `Gs. 1.234.567`.
- Never blame the user. Errors say what happened and what to do next.
- No exclamation stacking, max one "!" per screen. Emojis: only ✓ and 🔥 (streaks), nowhere else.
- Never use em dashes. Use commas, colons or parentheses.
- Dates: "19 de septiembre", short form "19 sep".
## 1. Common
```
common.appName = Impuestos (placeholder, rename at brand time)
common.continue = Continuar
common.back = Volver
common.save = Guardar
common.cancel = Cancelar
common.confirm = Confirmar
common.edit = Editar
common.delete = Eliminar
common.retry = Reintentar
common.close = Cerrar
common.loading = Cargando...
common.search = Buscar
common.seeDetail = Ver detalle
common.optional = (opcional)
common.error.generic = Algo salio mal de nuestro lado. Probá de nuevo en un momento.
common.error.offline = Sin conexion. Tus cambios se guardan y se sincronizan al volver.
common.status.alDia = Al dia
common.status.porVencer = Por vencer
common.status.enRevision = En revision
common.status.atrasado = Atrasado
```
## 2. Landing
```
landing.hero.title = Tus impuestos, en piloto automatico
landing.hero.subtitle = Escaneá tus facturas y nosotros armamos tus libros, tus deducciones y tus declaraciones. Nunca mas un vencimiento olvidado.
landing.hero.inputLabel = Ingresá tu RUC o CI
landing.hero.cta = Ver mi situacion
landing.value1.title = Escaneá y listo
landing.value1.body = Sacale una foto a cualquier factura. La leemos, la verificamos y la clasificamos por vos.
landing.value2.title = Sabé cuanto vas a pagar, siempre
landing.value2.body = Tu IVA del mes y tu IRP del año, calculados en vivo con cada factura que cargás.
landing.value3.title = Declaraciones listas para presentar
landing.value3.body = Tu Formulario 120 y tu 515 se arman solos. Vos solo revisás y aprobás.
landing.preview.title = Esto es lo que la DNIT ya sabe de vos
landing.preview.deadline = Tus vencimientos caen el dia {day} de cada mes
landing.preview.next = Proximos: {d1}, {d2} y {d3}
landing.preview.cta = Crear mi cuenta gratis
landing.invalidDoc = Ese numero no parece valido. Revisá el digito verificador (el numero despues del guion).
```
## 3. Auth
```
auth.register.title = Creá tu cuenta
auth.register.email = Tu email
auth.register.password = Elegí una contraseña
auth.login.title = Entrá a tu cuenta
auth.otp.title = Revisá tu email
auth.otp.body = Te enviamos un codigo de 6 digitos a {email}.
auth.otp.resend = Reenviar codigo
auth.logout = Cerrar sesion
```
## 4. Consent
```
consent.title = Antes de empezar, lo importante
consent.intro = Guardamos tus facturas y tus datos fiscales para armar tus impuestos. Nada mas, nada menos.
consent.bullet1 = Tus datos son tuyos: los podes descargar o borrar cuando quieras.
consent.bullet2 = Nunca vendemos ni compartimos tu informacion.
consent.bullet3 = Todo acceso de nuestro equipo a tus datos queda registrado.
consent.dataProcessing = Acepto el tratamiento de mis datos para este servicio
consent.notifications = Quiero recibir avisos de vencimientos y novedades
consent.policyLink = Leer la politica completa
```
## 5. Profile setup
```
setup.step1.title = Confirmá tus datos
setup.fullName = Nombre completo
setup.taxpayerKind.q = ¿Sos persona o empresa?
setup.taxpayerKind.ind = Persona fisica
setup.taxpayerKind.com = Empresa
setup.step2.title = ¿Que obligaciones tenes?
setup.oblig.iva.title = IVA mensual
setup.oblig.iva.body = Tengo RUC activo y facturo con IVA
setup.oblig.irp.title = IRP
setup.oblig.irp.body = Gano mas de Gs. 80 millones al año
setup.oblig.unsure = No estoy seguro
setup.income.q = ¿Cuanto estimás que vas a ganar este año?
setup.income.help = Sirve para proyectar tu IRP. Lo podés cambiar cuando quieras.
setup.dependents.title = Tus familiares a cargo
setup.dependents.help = Los gastos de tus familiares a cargo tambien pueden ser deducibles.
setup.dependents.add = Agregar familiar
setup.dependents.skip = Lo hago despues
setup.step3.title = Avisos
setup.push.title = Activá las notificaciones
setup.push.body = Un aviso a tiempo vale mas que mil recargos. Te avisamos solo lo importante.
setup.push.cta = Activar
setup.push.later = Ahora no
```
## 6. Dashboard (Mi situacion)
```
home.title = Mi situacion
home.iva.title = IVA de {month}
home.iva.aPagar = A pagar
home.iva.aFavor = A tu favor
home.iva.breakdown = Debito Gs. {debito}, credito Gs. {credito}
home.irp.title = IRP proyectado {year}
home.irp.delta = Bajaste Gs. {amount} este mes gracias a tus deducciones
home.irp.belowThreshold = Por ahora estas debajo del minimo de Gs. 80 millones. Esto es solo informativo.
home.next.allClear = Todo al dia ✓
home.next.upcoming = Proximo vencimiento: {date}
home.next.dueSoon = Vence tu {form} en {days} dias
home.next.reviewCta = Revisar y aprobar
home.next.bandeja = Tenes {count} facturas por revisar
home.next.bandejaCta = Ir a la bandeja
home.insight.gapTitle = Te estas perdiendo deducciones
home.insight.gapBody = Casi no cargaste facturas de {category} este año, y son deducibles.
home.insight.monthClose = Cerraste {month} con Gs. {savings} en deducciones nuevas.
home.firstRun.title = Empecemos con tu primera factura
home.firstRun.body = Escaneá cualquier factura que tengas a mano y mirá lo que pasa.
home.firstRun.scan = Escanear mi primera factura
home.firstRun.manual = O cargala a mano
```
## 7. Scan and manual entry
```
scan.fab = Escanear
scan.hint = Enfocá el QR de la factura
scan.noQrHint = ¿Factura sin QR? Sacale una foto igual
scan.upload = Subir archivo
scan.processing = Leyendo tu factura...
scan.verified = Verificado ✓
scan.registered = Registrada
scan.ocrLowConfidence = Revisá los campos marcados, no los pudimos leer bien.
scan.duplicate.title = Ya tenias esta factura
scan.duplicate.body = La registramos el {date}. No se duplica nada.
scan.result.suggested = Sugerimos: {category}
scan.result.confirm = Confirmar
scan.result.changeCat = Cambiar categoria
scan.result.discard = Descartar
manual.title = Cargar factura a mano
manual.emitterRuc = RUC del que emitio
manual.emitterName = Nombre o razon social
manual.total = Total
manual.ivaSplit.q = ¿Todo al 10%?
manual.date = Fecha
manual.saved = Factura guardada ✓
```
## 8. Bandeja
```
bandeja.title = Bandeja
bandeja.empty = Bandeja limpia ✓
bandeja.emptyBody = Cuando escanees o recibamos facturas nuevas, aparecen aca para que las confirmes.
bandeja.confirm = Confirmar
bandeja.reject.title = ¿Por que la descartas?
bandeja.reject.notMine = No es mia
bandeja.reject.duplicate = Esta duplicada
bandeja.reject.other = Otro motivo
bandeja.autoConfirmNote = Las facturas con alta confianza se confirman solas en {days} dias.
bandeja.autoConfirmLink = Cambiar
categories.alimentacion = Alimentacion
categories.salud = Salud
categories.educacion = Educacion
categories.vivienda = Vivienda
categories.vestimenta = Vestimenta
categories.esparcimiento = Esparcimiento
categories.vehiculo = Vehiculo
categories.familiares = Familiares
categories.none = Sin categoria
```
## 9. Declarations
```
decl.title = Declaraciones
decl.generate = Preparar {form} de {period}
decl.status.draft = Borrador
decl.status.ready = Lista para revisar
decl.status.approved = Aprobada
decl.f120.summary = Vendiste Gs. {sales} y compraste Gs. {purchases}. {result}
decl.f120.toPay = Te corresponde pagar Gs. {amount}.
decl.f120.inFavor = Te queda un saldo a favor de Gs. {amount} para el mes que viene.
decl.f515.storyTitle = Tu año {year}
decl.f515.capNote = Gs. {amount} no se pudieron deducir por el tope del 1% en compras a RESIMPLE.
decl.preview.draftMark = BORRADOR
decl.approve = Aprobar
decl.approve.confirmTitle = ¿Aprobas esta declaracion?
decl.approve.confirmBody = Vas a aprobar tu {form} de {period} por Gs. {amount}.
decl.downloadPdf = Descargar PDF
decl.checklist.title = Presentala en Marangatu
decl.checklist.intro = Segui estos pasos con tu clave de Marangatu. Los valores ya estan listos para copiar.
decl.checklist.copyValue = Copiar valor
decl.checklist.done = Ya la presente
decl.filed.title = Declaracion presentada ✓
decl.filed.body = Te avisamos antes de la fecha de pago. Buen trabajo.
decl.streak = {count} periodos seguidos al dia 🔥
```
## 10. Documents, deadlines, profile
```
docs.title = Comprobantes
docs.filter.month = Mes
docs.filter.category = Categoria
docs.detail.trail = Historial de cambios
vto.title = Vencimientos
vto.explainer = Por tu RUC terminado en {digit}, tus vencimientos caen el dia {day} de cada mes.
profile.title = Perfil
profile.myData.title = Tus datos
profile.myData.body = Esto es todo lo que guardamos sobre vos.
profile.myData.export = Descargar mis datos
profile.myData.delete = Eliminar mi cuenta
profile.myData.deleteWarn = Se borra todo: tus facturas, tus declaraciones y tu cuenta. No hay vuelta atras.
profile.consent.revoke = Revocar consentimiento
notif.digestHour = Hora del resumen diario
```
## 11. Notifications (templates)
```
push.digest = {bandejaCount} facturas por revisar · proximo vencimiento {date}
push.deadline.t2 = Pasado mañana vence tu {form}: Gs. {amount}
push.deadline.t0 = Hoy vence tu {form}: Gs. {amount}
push.declReady = Tu {form} de {period} esta lista para revisar
push.savings = Encontramos Gs. {amount} de credito nuevo este mes
email.subject.deadline = Vence tu {form} el {date}
telegram.linked = Listo, te aviso por aca. Solo lo importante.
```
## 12. Admin
```
admin.users.title = Usuarios
admin.users.auditBanner = Todos los accesos a datos de usuarios quedan registrados.
admin.errors.title = Errores de ingesta
admin.errors.resolve = Marcar resuelto
admin.audit.title = Auditoria
admin.audit.export = Exportar CSV
```
## 13. Legal pages (placeholders, human-written before launch)
`/legal/privacidad`, `/legal/terminos`: ship with clearly marked placeholder content ("Documento en preparacion") and a TODO in DECISIONS.md. Never generate fake legal text.