estela 0.1.19 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/LICENSE +1 -1
  2. package/dist/billing/amortize.d.ts.map +1 -1
  3. package/dist/billing/amortize.js +3 -2
  4. package/dist/billing/amortize.js.map +1 -1
  5. package/dist/billing/invoice.d.ts.map +1 -1
  6. package/dist/billing/invoice.js +19 -18
  7. package/dist/billing/invoice.js.map +1 -1
  8. package/dist/billing/sessionize.d.ts.map +1 -1
  9. package/dist/billing/sessionize.js +3 -2
  10. package/dist/billing/sessionize.js.map +1 -1
  11. package/dist/billing.d.ts.map +1 -1
  12. package/dist/billing.js +5 -4
  13. package/dist/billing.js.map +1 -1
  14. package/dist/bin.js +6 -2
  15. package/dist/bin.js.map +1 -1
  16. package/dist/cli.d.ts.map +1 -1
  17. package/dist/cli.js +311 -164
  18. package/dist/cli.js.map +1 -1
  19. package/dist/cloud/auth.d.ts.map +1 -1
  20. package/dist/cloud/auth.js +9 -8
  21. package/dist/cloud/auth.js.map +1 -1
  22. package/dist/cloud/client.d.ts.map +1 -1
  23. package/dist/cloud/client.js +11 -6
  24. package/dist/cloud/client.js.map +1 -1
  25. package/dist/db/schema.d.ts.map +1 -1
  26. package/dist/db/schema.js +4 -3
  27. package/dist/db/schema.js.map +1 -1
  28. package/dist/doctor.d.ts.map +1 -1
  29. package/dist/doctor.js +50 -45
  30. package/dist/doctor.js.map +1 -1
  31. package/dist/i18n/en.d.ts +10 -0
  32. package/dist/i18n/en.d.ts.map +1 -0
  33. package/dist/i18n/en.js +326 -0
  34. package/dist/i18n/en.js.map +1 -0
  35. package/dist/i18n/i18n.test.d.ts +2 -0
  36. package/dist/i18n/i18n.test.d.ts.map +1 -0
  37. package/dist/i18n/i18n.test.js +201 -0
  38. package/dist/i18n/i18n.test.js.map +1 -0
  39. package/dist/i18n/index.d.ts +54 -0
  40. package/dist/i18n/index.d.ts.map +1 -0
  41. package/dist/i18n/index.js +106 -0
  42. package/dist/i18n/index.js.map +1 -0
  43. package/dist/publish.d.ts.map +1 -1
  44. package/dist/publish.js +6 -5
  45. package/dist/publish.js.map +1 -1
  46. package/dist/server.d.ts +3 -0
  47. package/dist/server.d.ts.map +1 -1
  48. package/dist/server.js +50 -25
  49. package/dist/server.js.map +1 -1
  50. package/dist/server.test.d.ts +2 -0
  51. package/dist/server.test.d.ts.map +1 -0
  52. package/dist/server.test.js +125 -0
  53. package/dist/server.test.js.map +1 -0
  54. package/dist/setup.d.ts.map +1 -1
  55. package/dist/setup.js +8 -5
  56. package/dist/setup.js.map +1 -1
  57. package/dist/sync/team.d.ts.map +1 -1
  58. package/dist/sync/team.js +7 -6
  59. package/dist/sync/team.js.map +1 -1
  60. package/dist/sync.d.ts.map +1 -1
  61. package/dist/sync.js +7 -6
  62. package/dist/sync.js.map +1 -1
  63. package/dist/team.d.ts.map +1 -1
  64. package/dist/team.js +9 -8
  65. package/dist/team.js.map +1 -1
  66. package/dist/updateCheck.d.ts.map +1 -1
  67. package/dist/updateCheck.js +3 -2
  68. package/dist/updateCheck.js.map +1 -1
  69. package/dist/watchers/claude.d.ts.map +1 -1
  70. package/dist/watchers/claude.js +3 -2
  71. package/dist/watchers/claude.js.map +1 -1
  72. package/dist/web-ui.test.js +96 -7
  73. package/dist/web-ui.test.js.map +1 -1
  74. package/node_modules/@estela/shared/dist/money.d.ts +1 -0
  75. package/node_modules/@estela/shared/dist/money.d.ts.map +1 -1
  76. package/node_modules/@estela/shared/dist/money.js +9 -1
  77. package/node_modules/@estela/shared/dist/money.js.map +1 -1
  78. package/package.json +1 -1
  79. package/web/app.js +213 -223
  80. package/web/i18n.js +635 -0
  81. package/web/index.html +40 -34
  82. package/web/lib.js +33 -8
package/web/i18n.js ADDED
@@ -0,0 +1,635 @@
1
+ "use strict";
2
+
3
+ /* Textos de la interfaz, en español e inglés.
4
+ *
5
+ * Una tabla, no dos copias de la página. La landing sí va duplicada porque es
6
+ * texto casi sin lógica; aquí hay 1.600 líneas de comportamiento, y dos copias
7
+ * de eso se separan en el primer arreglo que se haga en una sola.
8
+ *
9
+ * El idioma se decide una vez, al cargar: el que se eligió a mano (guardado en
10
+ * el navegador) o, si no, el del navegador. Cambiarlo recarga la página: así
11
+ * todo lo que se pinta al arrancar sale ya en el idioma nuevo, sin tener que
12
+ * repintar cada pantalla a mano.
13
+ *
14
+ * Plurales: una entrada con { one, other } elige por `n`. En español y en
15
+ * inglés basta con eso; "1 día trabajados" era justo el fallo de concatenar
16
+ * una "s" condicional a una frase que tenía más de una palabra que concordar.
17
+ *
18
+ * Se carga como script normal en el navegador y con require() en los tests. */
19
+
20
+ (function (root) {
21
+ const STORAGE_KEY = "estela.lang";
22
+ const SUPPORTED = ["es", "en"];
23
+
24
+ const dict = {
25
+ es: {
26
+ // ── Cabecera ──
27
+ "tab.summary": "Resumen",
28
+ "tab.day": "Mi día",
29
+ "tab.team": "Equipo",
30
+ "tab.reports": "Informes",
31
+ "tab.projects": "Proyectos",
32
+ "sync.now": "Sincronizar",
33
+ "sync.now.title": "Sube tus horas a la nube y baja lo de tus otras máquinas",
34
+ "sync.syncing": "Sincronizando…",
35
+ "sync.partial": "{ok} sincronizados · {project}: {detail}",
36
+ "sync.done": { one: "1 proyecto sincronizado", other: "{n} proyectos sincronizados" },
37
+ "sync.none": "No hay proyectos con sync activado",
38
+ "refresh.label": "Actualizar",
39
+ "refresh.title": "Buscar trabajo nuevo (R)",
40
+ "refresh.searching": "Buscando…",
41
+ "refresh.failed": "No se pudo actualizar",
42
+ "upgrade.btn": "Actualizar a Pro o Teams",
43
+ "upgrade.cycle": "Ciclo de facturación",
44
+ "upgrade.monthly": "Mensual",
45
+ "upgrade.yearly": "Anual",
46
+ "upgrade.saving": "Anual: 2 meses gratis",
47
+ "upgrade.pro.monthly": "9€/mes · paneles y sync ilimitados",
48
+ "upgrade.pro.yearly": "90€/año · paneles y sync ilimitados",
49
+ "upgrade.teams.monthly": "15€/proyecto/mes · horas de equipo medidas",
50
+ "upgrade.teams.yearly": "150€/proyecto/año · horas de equipo medidas",
51
+ "upgrade.needsLogin": "Vincula tu cuenta primero: estela login --email tu@correo.com",
52
+ "banner.more": "Ver más →",
53
+ "banner.close": "Cerrar",
54
+ "lang.switch": "EN",
55
+ "lang.switch.title": "Switch to English",
56
+
57
+ // ── Comunes ──
58
+ "common.cancel": "Cancelar",
59
+ "common.save": "Guardar",
60
+ "common.showLess": "Ver menos",
61
+ "common.perHour": "/h",
62
+ "period.week": "Esta semana",
63
+ "period.month": "Este mes",
64
+ "period.quarter": "3 meses",
65
+ "period.all": "Todo lo pendiente",
66
+ "when.today": "hoy",
67
+ "when.yesterday": "ayer",
68
+ "when.daysAgo": "hace {n} días",
69
+ "blocks": { one: "1 bloque", other: "{n} bloques" },
70
+ "blocks.new": { one: "1 bloque nuevo", other: "{n} bloques nuevos" },
71
+ "needsLogin.cmd": "Hace falta vincular una cuenta: estela login --email tu@correo.com",
72
+
73
+ // ── Tipos de trabajo ──
74
+ "kind.development": "Desarrollo",
75
+ "kind.meeting": "Reunión",
76
+ "kind.research": "Investigación",
77
+ "kind.review": "Revisión",
78
+ "kind.travel": "Desplazamiento",
79
+ "kind.support": "Soporte",
80
+ "kind.other": "Otro",
81
+ "kind.employment": "nómina",
82
+ "kind.internal": "interno",
83
+
84
+ // ── Mi día ──
85
+ "day.prev": "Día anterior",
86
+ "day.next": "Día siguiente",
87
+ "day.goToday": "Volver a hoy",
88
+ "day.worked": "trabajadas",
89
+ "day.recorded": "registradas",
90
+ "day.inBlocks": { one: "en 1 bloque", other: "en {n} bloques" },
91
+ "day.toInvoiceByCurrency": "por facturar (por moneda)",
92
+ "day.toInvoice": "por facturar",
93
+ "day.noRate": "sin tarifa definida",
94
+ "day.noBillable": "sin trabajo facturable",
95
+ "day.pending": {
96
+ one: "Queda 1 bloque por revisar — llévame a él",
97
+ other: "Quedan {n} bloques por revisar — llévame al primero",
98
+ },
99
+ "day.approve": "Aprobar el día",
100
+ "day.emptyToday.title": "Todavía nada por aquí",
101
+ "day.emptyToday.body": "En cuanto trabajes con un agente en un repositorio configurado, aparecerá solo. Si ya trabajaste, trae lo de hoy:",
102
+ "day.empty.title": "Sin actividad este día",
103
+ "day.empty.body": "No hay ningún bloque de trabajo registrado.",
104
+ "row.manual": "añadido a mano",
105
+ "row.fromCommits": "deducido de commits",
106
+ "row.noRate": "sin tarifa",
107
+ "row.notBillable": "no facturable",
108
+ "row.ai": "Consumo de IA: <b>{amount}</b> en tarifa API equivalente.",
109
+ "row.invoiced": "Este bloque ya está facturado, así que no se puede editar.",
110
+ "row.billableOn": "Facturable",
111
+ "row.billableOff": "No facturable",
112
+ "field.what": "Qué hiciste",
113
+ "field.project": "Proyecto",
114
+ "field.minutes": "Minutos",
115
+ "add.toggle": "+ Añadir horas sin código",
116
+ "add.hint": "Desarrollo sin agente, reuniones, desplazamientos, investigación. Un import nunca las toca.",
117
+ "add.kind": "Tipo",
118
+ "add.whatPlaceholder": "Seguimiento semanal con el cliente",
119
+ "add.submit": "Añadir",
120
+ "toast.hoursAdded": "Horas añadidas",
121
+ "toast.saved": "Guardado",
122
+ "toast.blocksApproved": { one: "1 bloque aprobado", other: "{n} bloques aprobados" },
123
+
124
+ // ── Resumen ──
125
+ "summary.projects": { one: "1 proyecto con actividad", other: "{n} proyectos con actividad" },
126
+ "summary.days": { one: "1 día trabajado", other: "{n} días trabajados" },
127
+ "summary.value": "valor del trabajo",
128
+ "summary.valueByCurrency": "valor del trabajo (por moneda)",
129
+ "summary.kpiHours": "horas registradas",
130
+ "summary.kpiBillable": "de ellas facturables",
131
+ "summary.kpiPending": { one: "bloque por revisar", other: "bloques por revisar" },
132
+ "summary.kpiAi": "consumo de IA",
133
+ "summary.empty.title": "Sin actividad en este periodo",
134
+ "summary.empty.body": "Prueba con un rango más amplio, o comprueba que tus repositorios estén asignados a un proyecto.",
135
+ "summary.approveAll": "Hay <b>{n}</b> bloques de trabajo <b>tuyo</b> sin revisar en este periodo. Al configurar un proyecto entra de golpe todo su histórico. Pulsa el aviso de cada proyecto para ir a ellos.",
136
+ "summary.approveAll.btn": "Aprobar el periodo",
137
+ "summary.activity": "Actividad reciente",
138
+ "summary.nothingLeft": "Ya no queda nada por revisar ahí",
139
+ "summary.pendingHere": "{blocks} por revisar aquí · {more}",
140
+ "summary.moreDays": { one: "1 día más con pendientes", other: "{n} días más con pendientes" },
141
+ "summary.lastPending": "{blocks} por revisar — es el último día pendiente",
142
+ "project.pendingTag": "{n} por revisar →",
143
+ "project.pendingTitle": "Ir al día más reciente con bloques sin revisar",
144
+ "project.days": { one: "1 día", other: "{n} días" },
145
+ "project.lastSeen": "última vez {date}",
146
+
147
+ // ── Equipo ──
148
+ "team.people": { one: "1 persona", other: "{n} personas" },
149
+ "team.activeDays": { one: "1 día con actividad", other: "{n} días con actividad" },
150
+ "team.allProjects": "Todos los proyectos",
151
+ "team.byProject": "Por proyecto",
152
+ "team.th.project": "Proyecto",
153
+ "team.th.composition": "Composición",
154
+ "team.th.hours": "Horas",
155
+ "team.th.aiCost": "Coste IA",
156
+ "team.th.value": "Valor",
157
+ "team.th.status": "Estado",
158
+ "team.th.person": "Persona",
159
+ "team.th.precision": "Precisión",
160
+ "team.th.branches": "Ramas",
161
+ "team.th.lastSeen": "Última vez",
162
+ "team.legend.agent": "medido con agente",
163
+ "team.legend.commit": "estimado de commits",
164
+ "team.legend.manual": "anotado a mano",
165
+ "team.peopleTitle": "Personas",
166
+ "team.byRecent": "por actividad reciente",
167
+ "team.note": "Las horas de tus compañeros se estiman desde sus commits y <b>no entran</b> en los totales de arriba, que cuentan solo tu trabajo imputado. Si alguien instala Estela, su fila pasa a medida.",
168
+ "team.showMore": { one: "Ver 1 persona más", other: "Ver {n} personas más" },
169
+ "team.empty.body": "Prueba con un rango más amplio.",
170
+ "team.alert.budgetOver": "{project} ha superado su presupuesto de IA",
171
+ "team.alert.budgetNear": "{project} ha consumido el {pct}% de su presupuesto de IA",
172
+ "team.alert.budgetDetail": "{spent} de {budget} en este periodo.",
173
+ "team.alert.stale": {
174
+ one: "1 rama sin integrar desde hace más de dos semanas",
175
+ other: "{n} ramas sin integrar desde hace más de dos semanas",
176
+ },
177
+ "team.alert.staleDetail": "La más antigua, {name}, lleva {days} días. Puede ser trabajo parado o una revisión pendiente en el otro lado.",
178
+ "team.alert.pending": { one: "1 bloque sin revisar", other: "{n} bloques sin revisar" },
179
+ "team.alert.pendingDetail": "Hasta revisarlos no deberían salir en un informe.",
180
+ "team.kpi.hours": "horas del periodo",
181
+ "team.kpi.delta": "{delta} frente al periodo anterior",
182
+ "team.kpi.noPrevious": "sin periodo anterior con el que comparar",
183
+ "team.kpi.aiCost": "coste de IA",
184
+ "team.kpi.perHour": "{amount} por hora trabajada",
185
+ "team.kpi.billable": "facturable",
186
+ "team.kpi.ofTotal": "{part} de {total}",
187
+ "team.kpi.unmerged": { one: "rama sin integrar", other: "ramas sin integrar" },
188
+ "team.kpi.oldest": "la más antigua, {n} días",
189
+ "team.kpi.allMerged": "todo integrado",
190
+ "team.composition": "agente {agent} · commits {commit} · a mano {manual}",
191
+ "team.status.pending": "por revisar",
192
+ "team.status.ready": "listo",
193
+ "team.you": "tú",
194
+ "team.measured": "medido",
195
+ "team.estimated": "estimado",
196
+
197
+ // ── Proyectos ──
198
+ "projects.sub": "Qué repositorios pertenecen a quién y a qué tarifa. Subir un precio no reescribe lo ya trabajado: la tarifa nueva empieza el día que digas.",
199
+ "projects.stale.head": {
200
+ one: "Un panel compartido está desactualizado.",
201
+ other: "{n} paneles están desactualizados.",
202
+ },
203
+ "projects.stale.body": "Tu cliente está viendo datos anteriores a tu último trabajo. Esto no tiene que ver con los bloques por revisar: aquí solo salen los proyectos que has compartido, y cuenta lo que cambió desde que lo publicaste, no lo que te falta por aprobar.",
204
+ "projects.stale.item": "{name} — {blocks} desde que lo publicaste {when}",
205
+ "projects.stale.fix": "Pulsa <b>Actualizar lo compartido</b> en cada uno para volver a subirlo.",
206
+ "projects.meta": "{client} · {duration} en {blocks}",
207
+ "projects.noRepo": "Sin repositorio: no se le imputa nada todavía.",
208
+ "projects.orphanOption": "{name} — {turns} turnos, hasta {last}",
209
+ "projects.link": "Vincular",
210
+ "projects.noOrphans": "No hemos detectado ningún repositorio sin asignar. Trabaja un rato con tu agente y vuelve, o vincúlalo con <code>estela team repo --project {id} --add &lt;ruta&gt;</code>.",
211
+ "projects.empty.title": "Todavía no hay proyectos",
212
+ "projects.empty.body": "Crea el primero abajo y asígnale el repositorio donde trabajas.",
213
+ "projects.orphanMeta": "{turns} turnos · hasta {last}",
214
+ "share.update": "Actualizar lo compartido",
215
+ "share.share": "Compartir con el cliente",
216
+ "share.hintNew": "Genera un panel de solo lectura con enlace no adivinable.",
217
+ "share.hintStale": "<b class=\"stale-mark\">{blocks}</b> desde que lo publicaste.",
218
+ "share.hintFresh": "Publicado {when} · al día.",
219
+ "share.message": "Te comparto el avance del proyecto: {url}",
220
+ "share.subject": "Avance del proyecto",
221
+ "share.copy": "Copiar enlace",
222
+ "share.email": "Correo",
223
+ "share.byEmail": "O dáselo por su correo y se lo mandamos nosotros — además le aparece en su cuenta",
224
+ "share.emailsPlaceholder": "cliente@empresa.com, otro@empresa.com",
225
+ "share.hasAccount": "<b>ya tiene cuenta</b>, lo ve al entrar",
226
+ "share.notYet": "aún no ha entrado en getestela.dev/app",
227
+ "rate.changeTo": "Cambiar tarifa a",
228
+ "rate.historyItem": "{amount}/h desde {date}",
229
+ "rate.from": "{currency}/h desde",
230
+ "newProject.title": "Nuevo proyecto",
231
+ "newProject.orphans": {
232
+ one: "Tienes 1 repositorio con trabajo capturado y sin proyecto. Son horas que ya registraste pero todavía no puedes informar. Púlsalo para empezar:",
233
+ other: "Tienes {n} repositorios con trabajo capturado y sin proyecto. Son horas que ya registraste pero todavía no puedes informar. Pulsa uno para empezar:",
234
+ },
235
+ "newProject.repo": "Repositorio",
236
+ "newProject.pickRepo": "— elegir uno de los detectados —",
237
+ "newProject.name": "Nombre del proyecto",
238
+ "newProject.namePlaceholder": "App de Acme",
239
+ "newProject.rate": "Tarifa por hora",
240
+ "newProject.client": "Cliente existente",
241
+ "newProject.newClientOption": "— cliente nuevo —",
242
+ "newProject.newClient": "…o cliente nuevo",
243
+ "newProject.clientPlaceholder": "Acme S.L.",
244
+ "newProject.currency": "Moneda",
245
+ "newProject.submit": "Crear proyecto",
246
+ "newProject.note": "Tras crear el proyecto, ejecuta <code>estela import</code> para que el trabajo ya capturado de ese repositorio se impute.",
247
+ "author.ask": "¿Con cuál de estos correos commiteas tú en este repositorio? Si eliges mal, se capturarán los commits de otra persona.",
248
+ "toast.identitySaved": "Identidad guardada — ejecuta estela import",
249
+ "toast.projectCreated": "Proyecto creado",
250
+ "toast.linkedImporting": "Vinculado · importando, aparecerá en unos segundos",
251
+ "toast.linkedBlocks": "Vinculado · {blocks} imputados",
252
+ "toast.repoLinked": "Repositorio vinculado",
253
+ "toast.linkCopied": "Enlace copiado",
254
+ "toast.copyFailed": "No se pudo copiar",
255
+ "toast.sharedByEmail": "Compartido — le llega un correo",
256
+ "toast.sharedWithNobody": "Ya no se comparte con nadie",
257
+ "toast.linkedToAccount": "Vinculado a tu cuenta",
258
+ "toast.enterRate": "Escribe una tarifa",
259
+ "toast.rateSaved": "Tarifa guardada — lo ya trabajado conserva la anterior",
260
+ "publish.linked": "Panel vinculado",
261
+ "publish.published": "Panel publicado",
262
+ "publish.link": "Enlace:",
263
+ "publish.noPassword": "Quien tenga el enlace, entra: no hay contraseña.",
264
+ "publish.needsLogin.title": "Hace falta vincular una cuenta",
265
+ "publish.needsLogin.why": "Publicar aloja el panel en getestela.dev, y eso exige sesión:",
266
+ "publish.needsLogin.cmd": "estela login --email tu@correo.com",
267
+ "publish.needsLogin.once": "Se ejecuta una vez, en la terminal. Luego vuelve a pulsar aquí.",
268
+
269
+ // ── Informes ──
270
+ "reports.sub": "Respaldo del trabajo hecho: horas y commits, para adjuntar a tu factura o compartir con quien dirige el proyecto.",
271
+ "reports.outside": "Hay además <b>{duration}</b> pendientes fuera de este periodo, en {blocks}. Pulsa <b>{all}</b> para incluirlos.",
272
+ "reports.empty.title": "Nada que informar todavía",
273
+ "reports.empty.body": "Aquí aparecerán los proyectos con horas aprobadas y sin informar.",
274
+ "reports.search": "Buscar proyecto o cliente…",
275
+ "reports.noMatch": "Ningún proyecto coincide con «{q}».",
276
+ "reports.client": "{client} · {blocks} · todo lo pendiente desde {date}",
277
+ "reports.from": "Desde",
278
+ "reports.to": "Hasta",
279
+ "reports.author": "Tu nombre",
280
+ "reports.authorPlaceholder": "Ana García",
281
+ "reports.amounts": "Incluir importes",
282
+ "reports.download": "Descargar informe",
283
+ "reports.more": { one: "Ver 1 proyecto más", other: "Ver {n} proyectos más" },
284
+ "reports.note": "El informe recoge horas y commits. No es una factura: Estela no emite documentos fiscales, adjunta este respaldo a la tuya.",
285
+ "toast.reportDownloaded": "Informe descargado",
286
+ },
287
+
288
+ en: {
289
+ // ── Header ──
290
+ "tab.summary": "Summary",
291
+ "tab.day": "My day",
292
+ "tab.team": "Team",
293
+ "tab.reports": "Reports",
294
+ "tab.projects": "Projects",
295
+ "sync.now": "Sync",
296
+ "sync.now.title": "Upload your hours and pull in what your other machines recorded",
297
+ "sync.syncing": "Syncing…",
298
+ "sync.partial": "{ok} synced · {project}: {detail}",
299
+ "sync.done": { one: "1 project synced", other: "{n} projects synced" },
300
+ "sync.none": "No projects have sync enabled",
301
+ "refresh.label": "Refresh",
302
+ "refresh.title": "Look for new work (R)",
303
+ "refresh.searching": "Looking…",
304
+ "refresh.failed": "Couldn't refresh",
305
+ "upgrade.btn": "Upgrade to Pro or Teams",
306
+ "upgrade.cycle": "Billing cycle",
307
+ "upgrade.monthly": "Monthly",
308
+ "upgrade.yearly": "Yearly",
309
+ "upgrade.saving": "Yearly: 2 months free",
310
+ "upgrade.pro.monthly": "€9/month · unlimited panels and sync",
311
+ "upgrade.pro.yearly": "€90/year · unlimited panels and sync",
312
+ "upgrade.teams.monthly": "€15/project/month · measured team hours",
313
+ "upgrade.teams.yearly": "€150/project/year · measured team hours",
314
+ "upgrade.needsLogin": "Link your account first: estela login --email you@example.com",
315
+ "banner.more": "Learn more →",
316
+ "banner.close": "Close",
317
+ "lang.switch": "ES",
318
+ "lang.switch.title": "Cambiar a español",
319
+
320
+ // ── Common ──
321
+ "common.cancel": "Cancel",
322
+ "common.save": "Save",
323
+ "common.showLess": "Show less",
324
+ "common.perHour": "/h",
325
+ "period.week": "This week",
326
+ "period.month": "This month",
327
+ "period.quarter": "3 months",
328
+ "period.all": "Everything pending",
329
+ "when.today": "today",
330
+ "when.yesterday": "yesterday",
331
+ "when.daysAgo": "{n} days ago",
332
+ "blocks": { one: "1 block", other: "{n} blocks" },
333
+ "blocks.new": { one: "1 new block", other: "{n} new blocks" },
334
+ "needsLogin.cmd": "You need to link an account: estela login --email you@example.com",
335
+
336
+ // ── Kinds of work ──
337
+ "kind.development": "Development",
338
+ "kind.meeting": "Meeting",
339
+ "kind.research": "Research",
340
+ "kind.review": "Review",
341
+ "kind.travel": "Travel",
342
+ "kind.support": "Support",
343
+ "kind.other": "Other",
344
+ "kind.employment": "payroll",
345
+ "kind.internal": "internal",
346
+
347
+ // ── My day ──
348
+ "day.prev": "Previous day",
349
+ "day.next": "Next day",
350
+ "day.goToday": "Back to today",
351
+ "day.worked": "worked",
352
+ "day.recorded": "recorded",
353
+ "day.inBlocks": { one: "in 1 block", other: "in {n} blocks" },
354
+ "day.toInvoiceByCurrency": "to invoice (by currency)",
355
+ "day.toInvoice": "to invoice",
356
+ "day.noRate": "no rate set",
357
+ "day.noBillable": "no billable work",
358
+ "day.pending": {
359
+ one: "1 block left to review — take me there",
360
+ other: "{n} blocks left to review — take me to the first",
361
+ },
362
+ "day.approve": "Approve the day",
363
+ "day.emptyToday.title": "Nothing here yet",
364
+ "day.emptyToday.body": "As soon as you work with an agent in a configured repository, it shows up on its own. If you've already worked, pull in today's work:",
365
+ "day.empty.title": "No activity on this day",
366
+ "day.empty.body": "No work blocks recorded.",
367
+ "row.manual": "added by hand",
368
+ "row.fromCommits": "inferred from commits",
369
+ "row.noRate": "no rate",
370
+ "row.notBillable": "not billable",
371
+ "row.ai": "AI usage: <b>{amount}</b> at equivalent API pricing.",
372
+ "row.invoiced": "This block has already been invoiced, so it can't be edited.",
373
+ "row.billableOn": "Billable",
374
+ "row.billableOff": "Not billable",
375
+ "field.what": "What you did",
376
+ "field.project": "Project",
377
+ "field.minutes": "Minutes",
378
+ "add.toggle": "+ Add hours without code",
379
+ "add.hint": "Development without an agent, meetings, travel, research. An import never touches them.",
380
+ "add.kind": "Type",
381
+ "add.whatPlaceholder": "Weekly check-in with the client",
382
+ "add.submit": "Add",
383
+ "toast.hoursAdded": "Hours added",
384
+ "toast.saved": "Saved",
385
+ "toast.blocksApproved": { one: "1 block approved", other: "{n} blocks approved" },
386
+
387
+ // ── Summary ──
388
+ "summary.projects": { one: "1 project with activity", other: "{n} projects with activity" },
389
+ "summary.days": { one: "1 day worked", other: "{n} days worked" },
390
+ "summary.value": "value of work",
391
+ "summary.valueByCurrency": "value of work (by currency)",
392
+ "summary.kpiHours": "hours recorded",
393
+ "summary.kpiBillable": "of which billable",
394
+ "summary.kpiPending": { one: "block to review", other: "blocks to review" },
395
+ "summary.kpiAi": "AI usage",
396
+ "summary.empty.title": "No activity in this period",
397
+ "summary.empty.body": "Try a wider range, or check that your repositories are assigned to a project.",
398
+ "summary.approveAll": "There are <b>{n}</b> blocks of <b>your</b> work unreviewed in this period. Setting up a project brings in its whole history at once. Click each project's notice to go to them.",
399
+ "summary.approveAll.btn": "Approve the period",
400
+ "summary.activity": "Recent activity",
401
+ "summary.nothingLeft": "Nothing left to review there",
402
+ "summary.pendingHere": "{blocks} to review here · {more}",
403
+ "summary.moreDays": { one: "1 more day with pending blocks", other: "{n} more days with pending blocks" },
404
+ "summary.lastPending": "{blocks} to review — this is the last pending day",
405
+ "project.pendingTag": "{n} to review →",
406
+ "project.pendingTitle": "Go to the most recent day with unreviewed blocks",
407
+ "project.days": { one: "1 day", other: "{n} days" },
408
+ "project.lastSeen": "last {date}",
409
+
410
+ // ── Team ──
411
+ "team.people": { one: "1 person", other: "{n} people" },
412
+ "team.activeDays": { one: "1 day with activity", other: "{n} days with activity" },
413
+ "team.allProjects": "All projects",
414
+ "team.byProject": "By project",
415
+ "team.th.project": "Project",
416
+ "team.th.composition": "Composition",
417
+ "team.th.hours": "Hours",
418
+ "team.th.aiCost": "AI cost",
419
+ "team.th.value": "Value",
420
+ "team.th.status": "Status",
421
+ "team.th.person": "Person",
422
+ "team.th.precision": "Precision",
423
+ "team.th.branches": "Branches",
424
+ "team.th.lastSeen": "Last seen",
425
+ "team.legend.agent": "measured with an agent",
426
+ "team.legend.commit": "estimated from commits",
427
+ "team.legend.manual": "logged by hand",
428
+ "team.peopleTitle": "People",
429
+ "team.byRecent": "by recent activity",
430
+ "team.note": "Your teammates' hours are estimated from their commits and <b>aren't included</b> in the totals above, which count only your own assigned work. If someone installs Estela, their row becomes measured.",
431
+ "team.showMore": { one: "Show 1 more person", other: "Show {n} more people" },
432
+ "team.empty.body": "Try a wider range.",
433
+ "team.alert.budgetOver": "{project} has gone over its AI budget",
434
+ "team.alert.budgetNear": "{project} has used {pct}% of its AI budget",
435
+ "team.alert.budgetDetail": "{spent} of {budget} in this period.",
436
+ "team.alert.stale": {
437
+ one: "1 branch unmerged for more than two weeks",
438
+ other: "{n} branches unmerged for more than two weeks",
439
+ },
440
+ "team.alert.staleDetail": "The oldest, {name}, has been open for {days} days. It may be stalled work or a review waiting on the other side.",
441
+ "team.alert.pending": { one: "1 unreviewed block", other: "{n} unreviewed blocks" },
442
+ "team.alert.pendingDetail": "Until they're reviewed, they shouldn't go into a report.",
443
+ "team.kpi.hours": "hours this period",
444
+ "team.kpi.delta": "{delta} vs. the previous period",
445
+ "team.kpi.noPrevious": "no previous period to compare with",
446
+ "team.kpi.aiCost": "AI cost",
447
+ "team.kpi.perHour": "{amount} per hour worked",
448
+ "team.kpi.billable": "billable",
449
+ "team.kpi.ofTotal": "{part} of {total}",
450
+ "team.kpi.unmerged": { one: "unmerged branch", other: "unmerged branches" },
451
+ "team.kpi.oldest": "oldest: {n} days",
452
+ "team.kpi.allMerged": "everything merged",
453
+ "team.composition": "agent {agent} · commits {commit} · by hand {manual}",
454
+ "team.status.pending": "to review",
455
+ "team.status.ready": "ready",
456
+ "team.you": "you",
457
+ "team.measured": "measured",
458
+ "team.estimated": "estimated",
459
+
460
+ // ── Projects ──
461
+ "projects.sub": "Which repositories belong to whom, and at what rate. Raising a price doesn't rewrite past work: the new rate starts on the day you choose.",
462
+ "projects.stale.head": {
463
+ one: "One shared panel is out of date.",
464
+ other: "{n} shared panels are out of date.",
465
+ },
466
+ "projects.stale.body": "Your client is looking at data from before your latest work. This has nothing to do with blocks to review: only projects you've shared show up here, and it counts what changed since you published, not what you still have to approve.",
467
+ "projects.stale.item": "{name} — {blocks} since you published it {when}",
468
+ "projects.stale.fix": "Click <b>Update shared panel</b> on each one to upload it again.",
469
+ "projects.meta": "{client} · {duration} in {blocks}",
470
+ "projects.noRepo": "No repository: nothing is assigned to it yet.",
471
+ "projects.orphanOption": "{name} — {turns} turns, until {last}",
472
+ "projects.link": "Link",
473
+ "projects.noOrphans": "We haven't detected any unassigned repository. Work with your agent for a while and come back, or link it with <code>estela team repo --project {id} --add &lt;path&gt;</code>.",
474
+ "projects.empty.title": "No projects yet",
475
+ "projects.empty.body": "Create the first one below and assign it the repository you work in.",
476
+ "projects.orphanMeta": "{turns} turns · until {last}",
477
+ "share.update": "Update shared panel",
478
+ "share.share": "Share with the client",
479
+ "share.hintNew": "Creates a read-only panel with an unguessable link.",
480
+ "share.hintStale": "<b class=\"stale-mark\">{blocks}</b> since you published it.",
481
+ "share.hintFresh": "Published {when} · up to date.",
482
+ "share.message": "Here's the progress on the project: {url}",
483
+ "share.subject": "Project progress",
484
+ "share.copy": "Copy link",
485
+ "share.email": "Email",
486
+ "share.byEmail": "Or give us their email and we'll send it — it also shows up in their account",
487
+ "share.emailsPlaceholder": "client@company.com, another@company.com",
488
+ "share.hasAccount": "<b>already has an account</b>, sees it when signing in",
489
+ "share.notYet": "hasn't signed in to getestela.dev/app yet",
490
+ "rate.changeTo": "Change rate to",
491
+ "rate.historyItem": "{amount}/h from {date}",
492
+ "rate.from": "{currency}/h from",
493
+ "newProject.title": "New project",
494
+ "newProject.orphans": {
495
+ one: "You have 1 repository with captured work and no project. Those are hours you've already recorded but can't report yet. Click it to start:",
496
+ other: "You have {n} repositories with captured work and no project. Those are hours you've already recorded but can't report yet. Click one to start:",
497
+ },
498
+ "newProject.repo": "Repository",
499
+ "newProject.pickRepo": "— pick one of the detected ones —",
500
+ "newProject.name": "Project name",
501
+ "newProject.namePlaceholder": "Acme app",
502
+ "newProject.rate": "Hourly rate",
503
+ "newProject.client": "Existing client",
504
+ "newProject.newClientOption": "— new client —",
505
+ "newProject.newClient": "…or new client",
506
+ "newProject.clientPlaceholder": "Acme Inc.",
507
+ "newProject.currency": "Currency",
508
+ "newProject.submit": "Create project",
509
+ "newProject.note": "After creating the project, run <code>estela import</code> so the work already captured from that repository gets assigned.",
510
+ "author.ask": "Which of these emails do you commit with in this repository? Pick the wrong one and you'll capture someone else's commits.",
511
+ "toast.identitySaved": "Identity saved — run estela import",
512
+ "toast.projectCreated": "Project created",
513
+ "toast.linkedImporting": "Linked · importing, it'll show up in a few seconds",
514
+ "toast.linkedBlocks": "Linked · {blocks} assigned",
515
+ "toast.repoLinked": "Repository linked",
516
+ "toast.linkCopied": "Link copied",
517
+ "toast.copyFailed": "Couldn't copy",
518
+ "toast.sharedByEmail": "Shared — they'll get an email",
519
+ "toast.sharedWithNobody": "No longer shared with anyone",
520
+ "toast.linkedToAccount": "Linked to your account",
521
+ "toast.enterRate": "Enter a rate",
522
+ "toast.rateSaved": "Rate saved — past work keeps the previous one",
523
+ "publish.linked": "Panel linked",
524
+ "publish.published": "Panel published",
525
+ "publish.link": "Link:",
526
+ "publish.noPassword": "Anyone with the link can get in: there's no password.",
527
+ "publish.needsLogin.title": "You need to link an account",
528
+ "publish.needsLogin.why": "Publishing hosts the panel on getestela.dev, which requires signing in:",
529
+ "publish.needsLogin.cmd": "estela login --email you@example.com",
530
+ "publish.needsLogin.once": "You only run it once, in the terminal. Then click here again.",
531
+
532
+ // ── Reports ──
533
+ "reports.sub": "Evidence of the work done: hours and commits, to attach to your invoice or share with whoever runs the project.",
534
+ "reports.outside": "There's also <b>{duration}</b> pending outside this period, in {blocks}. Click <b>{all}</b> to include it.",
535
+ "reports.empty.title": "Nothing to report yet",
536
+ "reports.empty.body": "Projects with approved, unreported hours will show up here.",
537
+ "reports.search": "Search project or client…",
538
+ "reports.noMatch": "No project matches “{q}”.",
539
+ "reports.client": "{client} · {blocks} · everything pending since {date}",
540
+ "reports.from": "From",
541
+ "reports.to": "To",
542
+ "reports.author": "Your name",
543
+ "reports.authorPlaceholder": "Jane Doe",
544
+ "reports.amounts": "Include amounts",
545
+ "reports.download": "Download report",
546
+ "reports.more": { one: "Show 1 more project", other: "Show {n} more projects" },
547
+ "reports.note": "The report covers hours and commits. It isn't an invoice: Estela doesn't issue tax documents, so attach it as backup to your own.",
548
+ "toast.reportDownloaded": "Report downloaded",
549
+ },
550
+ };
551
+
552
+ /** El idioma guardado a mano gana; si no hay, el del navegador; si no, inglés. */
553
+ function detectLang(stored, navigatorLangs) {
554
+ if (SUPPORTED.includes(stored)) return stored;
555
+ for (const tag of navigatorLangs || []) {
556
+ const base = String(tag).toLowerCase().split("-")[0];
557
+ if (SUPPORTED.includes(base)) return base;
558
+ }
559
+ return "en";
560
+ }
561
+
562
+ let lang = "es";
563
+
564
+ /**
565
+ * Texto de una clave, con sus variables sustituidas.
566
+ *
567
+ * Una clave que falta en inglés cae al español en vez de enseñar la clave
568
+ * pelada: mejor una frase en el otro idioma que "team.kpi.oldest" en
569
+ * pantalla. El test comprueba que no falte ninguna, así que esto es solo la
570
+ * red de seguridad.
571
+ */
572
+ function t(key, vars) {
573
+ let entry = dict[lang][key];
574
+ if (entry === undefined) entry = dict.es[key];
575
+ if (entry === undefined) return key;
576
+ if (typeof entry === "object") {
577
+ entry = vars && Number(vars.n) === 1 ? entry.one : entry.other;
578
+ }
579
+ return entry.replace(/\{(\w+)\}/g, (m, name) =>
580
+ vars && vars[name] !== undefined ? String(vars[name]) : m);
581
+ }
582
+
583
+ function setLang(next) {
584
+ lang = SUPPORTED.includes(next) ? next : "es";
585
+ return lang;
586
+ }
587
+
588
+ function getLang() { return lang; }
589
+
590
+ /**
591
+ * Traduce lo que viene escrito en el HTML de partida.
592
+ *
593
+ * data-i18n="clave" → el texto del elemento
594
+ * data-i18n-title / -aria-label / -placeholder → ese atributo
595
+ * data-i18n-monthly / -yearly → los data-monthly/yearly del menú de planes
596
+ */
597
+ function applyStatic(doc) {
598
+ doc.documentElement.lang = lang;
599
+ doc.querySelectorAll("[data-i18n]").forEach((el) => { el.textContent = t(el.dataset.i18n); });
600
+ for (const attr of ["title", "aria-label", "placeholder"]) {
601
+ doc.querySelectorAll(`[data-i18n-${attr}]`).forEach((el) => {
602
+ el.setAttribute(attr, t(el.getAttribute(`data-i18n-${attr}`)));
603
+ });
604
+ }
605
+ doc.querySelectorAll("[data-i18n-monthly]").forEach((el) => {
606
+ el.dataset.monthly = t(el.dataset.i18nMonthly);
607
+ el.dataset.yearly = t(el.dataset.i18nYearly);
608
+ });
609
+ }
610
+
611
+ const api = { dict, SUPPORTED, detectLang, t, setLang, getLang, applyStatic };
612
+
613
+ if (typeof module !== "undefined" && module.exports) {
614
+ module.exports = api;
615
+ return;
616
+ }
617
+
618
+ // En el navegador: decidir el idioma, avisar a lib.js para fechas e
619
+ // importes, y traducir la página de partida antes de que app.js pinte nada.
620
+ let stored = null;
621
+ try { stored = root.localStorage.getItem(STORAGE_KEY); } catch { /* sin almacenamiento */ }
622
+ setLang(detectLang(stored, root.navigator && root.navigator.languages));
623
+ if (typeof root.setLocale === "function") root.setLocale(lang);
624
+
625
+ // `tr` y no `t` en la página: app.js usa `t` como nombre de variable en
626
+ // varias funciones (`const t = state.team`), y ahí taparía a esta.
627
+ Object.assign(root, { tr: t, getLang });
628
+ root.switchLang = function switchLang() {
629
+ const next = lang === "es" ? "en" : "es";
630
+ try { root.localStorage.setItem(STORAGE_KEY, next); } catch { /* sin almacenamiento */ }
631
+ root.location.reload();
632
+ };
633
+
634
+ applyStatic(root.document);
635
+ })(typeof globalThis !== "undefined" ? globalThis : this);