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>
255 lines
13 KiB
Markdown
255 lines
13 KiB
Markdown
# 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.
|