From f298629c43ade3c6a69041ac950e180d649adcd5 Mon Sep 17 00:00:00 2001 From: santidev21 Date: Tue, 6 Oct 2026 10:23:15 -0500 Subject: [PATCH] feat(i18n): translate the motorcycle flow (3.6) Third i18n surface: the motorcycle flow. Moves the strings of the dashboard shell banner, home, motorcycle card, add/edit form, summary and log-record page into the dictionaries, including the confirm/success/error dialogs. Also widens the `t()` params to `unknown` (templates pass pipe results that are typed `string | null`), stringified at interpolation time. Remaining (final PR): maintenance page, save/follow maintenance, statistics, profile and the legal/help long-form text. --- .../dashboard-layout.component.html | 2 +- .../dashboard-layout.component.ts | 4 +- .../motorcycle-card.component.html | 8 +- .../motorcycle-card.component.ts | 21 +-- .../dashboard/pages/home/home.component.html | 14 +- .../dashboard/pages/home/home.component.ts | 4 +- .../motorcycle-summary.component.html | 112 +++++++++----- .../motorcycle-summary.component.ts | 62 +++++--- ...register-maintenance-record.component.html | 32 ++-- .../register-maintenance-record.component.ts | 18 ++- .../save-motorcycle.component.html | 89 ++++++----- .../save-motorcycle.component.ts | 38 +++-- .../src/app/shared/i18n/i18n.service.ts | 2 +- .../src/app/shared/i18n/translate.pipe.ts | 2 +- .../src/app/shared/i18n/translations/en.ts | 139 +++++++++++++++++ .../src/app/shared/i18n/translations/es.ts | 140 ++++++++++++++++++ docs/specs/i18n.md | 8 +- 17 files changed, 529 insertions(+), 166 deletions(-) diff --git a/bikontrol-web/src/app/modules/dashboard/components/dashboard-layout/dashboard-layout.component.html b/bikontrol-web/src/app/modules/dashboard/components/dashboard-layout/dashboard-layout.component.html index c234ce7..9adcfad 100644 --- a/bikontrol-web/src/app/modules/dashboard/components/dashboard-layout/dashboard-layout.component.html +++ b/bikontrol-web/src/app/modules/dashboard/components/dashboard-layout/dashboard-layout.component.html @@ -4,7 +4,7 @@
- Estás viendo la demo en modo solo lectura — los cambios están deshabilitados. + {{ 'dash.demoBanner' | t }}
} diff --git a/bikontrol-web/src/app/modules/dashboard/components/dashboard-layout/dashboard-layout.component.ts b/bikontrol-web/src/app/modules/dashboard/components/dashboard-layout/dashboard-layout.component.ts index abb63b2..78181d5 100644 --- a/bikontrol-web/src/app/modules/dashboard/components/dashboard-layout/dashboard-layout.component.ts +++ b/bikontrol-web/src/app/modules/dashboard/components/dashboard-layout/dashboard-layout.component.ts @@ -5,9 +5,11 @@ import { BottomNavComponent } from '../../../../shared/components/bottom-nav/bot import { RouterOutlet } from '@angular/router'; import { AuthService } from '../../../auth/services/auth.service'; +import { TranslatePipe } from '../../../../shared/i18n/translate.pipe'; + @Component({ selector: 'app-dashboard-layout', - imports: [TopNavComponent, BottomNavComponent, RouterOutlet], + imports: [TopNavComponent, BottomNavComponent, RouterOutlet, TranslatePipe], templateUrl: './dashboard-layout.component.html', changeDetection: ChangeDetectionStrategy.Eager, styleUrl: './dashboard-layout.component.scss', diff --git a/bikontrol-web/src/app/modules/dashboard/components/motorcycle-card/motorcycle-card.component.html b/bikontrol-web/src/app/modules/dashboard/components/motorcycle-card/motorcycle-card.component.html index e67da3b..39ce125 100644 --- a/bikontrol-web/src/app/modules/dashboard/components/motorcycle-card/motorcycle-card.component.html +++ b/bikontrol-web/src/app/modules/dashboard/components/motorcycle-card/motorcycle-card.component.html @@ -25,7 +25,9 @@ {{ motorcycle.name }}

{{ motorcycle.brand }} - {{ motorcycle.year }}

-

Apodo: {{ motorcycle.nickname }}

+

+ {{ 'motoCard.nickname' | t: { name: motorcycle.nickname } }} +

route icon @@ -66,7 +68,7 @@ 85-28-29 57 57-29-28Z" /> - Editar + {{ 'motoCard.edit' | t }} } diff --git a/bikontrol-web/src/app/modules/dashboard/components/motorcycle-card/motorcycle-card.component.ts b/bikontrol-web/src/app/modules/dashboard/components/motorcycle-card/motorcycle-card.component.ts index 3b5dbf2..9bc5c67 100644 --- a/bikontrol-web/src/app/modules/dashboard/components/motorcycle-card/motorcycle-card.component.ts +++ b/bikontrol-web/src/app/modules/dashboard/components/motorcycle-card/motorcycle-card.component.ts @@ -16,9 +16,12 @@ import { SwalService } from '../../../../shared/services/swal.service'; import { HttpErrorService } from '../../../../shared/services/http-error.service'; import { AuthService } from '../../../auth/services/auth.service'; +import { TranslatePipe } from '../../../../shared/i18n/translate.pipe'; +import { I18nService } from '../../../../shared/i18n/i18n.service'; + @Component({ selector: 'app-motorcycle-card', - imports: [], + imports: [TranslatePipe], templateUrl: './motorcycle-card.component.html', changeDetection: ChangeDetectionStrategy.OnPush, styleUrl: './motorcycle-card.component.scss', @@ -29,6 +32,7 @@ export class MotorcycleCardComponent implements OnInit { private swal = inject(SwalService); private httpError = inject(HttpErrorService); private authService = inject(AuthService); + private i18n = inject(I18nService); @Input() motorcycle!: Motorcycle; @Output() deleted = new EventEmitter(); @@ -67,14 +71,11 @@ export class MotorcycleCardComponent implements OnInit { this.motorcyclesService.deleteMotorcycle(this.motorcycle.id!).subscribe({ next: () => { this.swal - .success('¡Eliminada!', 'La motocicleta fue eliminada correctamente.') + .success(this.i18n.t('motoCard.deletedTitle'), this.i18n.t('motoCard.deletedText')) .then(() => this.deleted.emit()); }, error: (err) => { - this.swal.error( - 'Error', - this.httpError.message(err, 'No se pudo eliminar la motocicleta.'), - ); + this.swal.error('Error', this.httpError.message(err, this.i18n.t('motoCard.deleteError'))); }, }); } @@ -84,10 +85,10 @@ export class MotorcycleCardComponent implements OnInit { this.swal .confirm( - '¿Estás seguro?', - `Esto eliminará permanentemente "${this.motorcycle.name}".`, - 'Sí, eliminar', - 'Cancelar', + this.i18n.t('motoCard.deleteConfirmTitle'), + this.i18n.t('motoCard.deleteConfirmText', { name: this.motorcycle.name }), + this.i18n.t('common.yesDelete'), + this.i18n.t('common.cancel'), 'warning', ) .then((result) => { diff --git a/bikontrol-web/src/app/modules/dashboard/pages/home/home.component.html b/bikontrol-web/src/app/modules/dashboard/pages/home/home.component.html index 5768441..7cd823f 100644 --- a/bikontrol-web/src/app/modules/dashboard/pages/home/home.component.html +++ b/bikontrol-web/src/app/modules/dashboard/pages/home/home.component.html @@ -1,19 +1,17 @@

-

Bienvenido de nuevo

-

Tus motocicletas

+

{{ 'home.welcome' | t }}

+

{{ 'home.subtitle' | t }}

@if (motorcycles.value().length === 0) {
-

¡Te damos la bienvenida!

-

- Agregá tu moto y elegí un plan de mantenimiento en un par de minutos. -

+

{{ 'home.emptyTitle' | t }}

+

{{ 'home.emptyText' | t }}

@if (!isDemo) { }
@@ -34,7 +32,7 @@

¡Te damos la bienvenida!

routerLink="/dashboard/motorcycles/add" class="w-full mt-6 py-2 bg-primary text-textlight rounded-lg hover:bg-accent transition-colors" > - + Agregar motocicleta + {{ 'home.addMotorcycle' | t }}
} diff --git a/bikontrol-web/src/app/modules/dashboard/pages/home/home.component.ts b/bikontrol-web/src/app/modules/dashboard/pages/home/home.component.ts index 5dc9fa8..680dca6 100644 --- a/bikontrol-web/src/app/modules/dashboard/pages/home/home.component.ts +++ b/bikontrol-web/src/app/modules/dashboard/pages/home/home.component.ts @@ -6,9 +6,11 @@ import { MotorcyclesService } from '../../service/motorcycles.service'; import { SwalService } from '../../../../shared/services/swal.service'; import { HttpErrorService } from '../../../../shared/services/http-error.service'; +import { TranslatePipe } from '../../../../shared/i18n/translate.pipe'; + @Component({ selector: 'app-home', - imports: [MotorcycleCardComponent, RouterModule], + imports: [MotorcycleCardComponent, RouterModule, TranslatePipe], templateUrl: './home.component.html', changeDetection: ChangeDetectionStrategy.OnPush, styleUrl: './home.component.scss', diff --git a/bikontrol-web/src/app/modules/dashboard/pages/motorcycles/motorcycle-summary/motorcycle-summary.component.html b/bikontrol-web/src/app/modules/dashboard/pages/motorcycles/motorcycle-summary/motorcycle-summary.component.html index 3538f2d..30c91e9 100644 --- a/bikontrol-web/src/app/modules/dashboard/pages/motorcycles/motorcycle-summary/motorcycle-summary.component.html +++ b/bikontrol-web/src/app/modules/dashboard/pages/motorcycles/motorcycle-summary/motorcycle-summary.component.html @@ -16,7 +16,7 @@ {{ m.brand }}

{{ m.name }}

- {{ m.nickname || 'Sin apodo' }} + {{ m.nickname || ('motoCard.noNickname' | t) }} @@ -24,9 +24,13 @@

{{ m.name }}

class="km-card bg-gray-800 rounded-xl shadow-md p-4 my-6 hover:shadow-lg transition-shadow relative" >
- Kilometraje Actual + {{ 'summary.currentKm' | t }} @if (!isDemo()) { -
@@ -58,7 +62,7 @@

{{ m.name }}

[disabled]="isExporting()" class="flex-1 py-2 border border-accent text-primary rounded-lg hover:bg-gray-50 transition-colors text-sm font-semibold disabled:opacity-50" > - Exportar libro (PDF) + {{ 'summary.exportPdf' | t }}
-

Proximos Mantenimientos

+

{{ 'summary.upcoming' | t }}

@if (!isDemo()) { } @if (upcomingMaintenances().length > 0) { @@ -107,48 +111,57 @@

{{ maintenance.name }}

@if (maintenance.trackingType === 'Km') {
{{ - maintenance.isOverdue - ? 'Vencido por ' + maintenance.remainingKm * -1 + ' km' - : 'Faltan ' + maintenance.remainingKm + ' km' + (maintenance.isOverdue ? 'summary.overdueKm' : 'summary.remainingKm') + | t + : { + n: maintenance.isOverdue + ? maintenance.remainingKm * -1 + : maintenance.remainingKm, + } }}
} @if (maintenance.trackingType === 'Time') {
{{ - maintenance.isOverdue - ? 'Vencido por ' + maintenance.remainingDays * -1 + ' día(s)' - : 'Faltan ' + maintenance.remainingDays + ' día(s)' + (maintenance.isOverdue ? 'summary.overdueDays' : 'summary.remainingDays') + | t + : { + n: maintenance.isOverdue + ? maintenance.remainingDays * -1 + : maintenance.remainingDays, + } }}
}
@if (maintenance.lastPerformedAt) {

- Último mantenimiento: {{ maintenance.lastPerformedAt | date: 'yyyy-MM-dd' }} + {{ + 'summary.lastPerformed' + | t: { date: (maintenance.lastPerformedAt | date: 'yyyy-MM-dd') } + }}

} } } @else { -

- Aún no has agregado mantenimientos para esta moto. -

+

{{ 'summary.noMaintenances' | t }}

} @if (!isDemo()) { }
@defer (on viewport) {
-

Registro de mantenimientos

+

{{ 'summary.records' | t }}

@if (maintenanceRecords().length > 0) {
@for (record of maintenanceRecords(); track record) { @@ -160,25 +173,35 @@

{{ record.maintenanceName }}

}}
@if (record.performedKm != null) { -

Km: {{ record.performedKm }}

+

+ {{ 'summary.recordKm' | t: { n: record.performedKm } }} +

} @if (record.cost != null) {

- Costo: {{ record.cost | currency: 'COP' : 'symbol-narrow' : '1.0-2' }} + {{ + 'summary.recordCost' + | t: { c: (record.cost | currency: 'COP' : 'symbol-narrow' : '1.0-2') } + }}

}
- Fotos y documentos + {{ 'summary.photos' | t }} @if (!isDemo()) {
} @else { -

- Aún no hay registros de mantenimientos para esta moto. -

+

{{ 'summary.noRecords' | t }}

}
} @placeholder { -

Cargando registros…

+

{{ 'summary.loadingRecords' | t }}

}
} @else {
-

No se encontró información de la motocicleta.

-

Serás redirigido al inicio en breve.

+

{{ 'summary.notFound' | t }}

+

{{ 'summary.redirecting' | t }}

} @if (isEditKmModalOpen()) { } @if (viewerImage(); as image) { - diff --git a/bikontrol-web/src/app/modules/dashboard/pages/motorcycles/motorcycle-summary/motorcycle-summary.component.ts b/bikontrol-web/src/app/modules/dashboard/pages/motorcycles/motorcycle-summary/motorcycle-summary.component.ts index 740f561..2fc74a2 100644 --- a/bikontrol-web/src/app/modules/dashboard/pages/motorcycles/motorcycle-summary/motorcycle-summary.component.ts +++ b/bikontrol-web/src/app/modules/dashboard/pages/motorcycles/motorcycle-summary/motorcycle-summary.component.ts @@ -25,9 +25,12 @@ import { HttpErrorService } from '../../../../../shared/services/http-error.serv import { ImageService } from '../../../../../shared/services/image.service'; import { AuthService } from '../../../../auth/services/auth.service'; +import { TranslatePipe } from '../../../../../shared/i18n/translate.pipe'; +import { I18nService } from '../../../../../shared/i18n/i18n.service'; + @Component({ selector: 'app-motorcycle-summary', - imports: [CommonModule, RouterModule, FormsModule], + imports: [CommonModule, RouterModule, FormsModule, TranslatePipe], templateUrl: './motorcycle-summary.component.html', changeDetection: ChangeDetectionStrategy.OnPush, styleUrl: './motorcycle-summary.component.scss', @@ -39,6 +42,7 @@ export class MotorcycleSummaryComponent implements OnInit, OnDestroy { private readonly motorcyclesService = inject(MotorcyclesService); private readonly swal = inject(SwalService); private readonly httpError = inject(HttpErrorService); + private readonly i18n = inject(I18nService); private readonly imageService = inject(ImageService); private readonly authService = inject(AuthService); @@ -88,8 +92,8 @@ export class MotorcycleSummaryComponent implements OnInit, OnDestroy { const error = this.upcomingRes.error(); if (error) { this.swal.error( - 'Error', - this.httpError.message(error, 'No se pudieron cargar los mantenimientos próximos.'), + this.i18n.t('common.error'), + this.httpError.message(error, this.i18n.t('summary.upcomingError')), ); } }); @@ -105,8 +109,8 @@ export class MotorcycleSummaryComponent implements OnInit, OnDestroy { const error = this.recordsRes.error(); if (error) { this.swal.error( - 'Error', - this.httpError.message(error, 'No se pudieron cargar los registros de mantenimiento.'), + this.i18n.t('common.error'), + this.httpError.message(error, this.i18n.t('summary.recordsError')), ); } }); @@ -184,14 +188,22 @@ export class MotorcycleSummaryComponent implements OnInit, OnDestroy { this.setAttachments(recordId, [...this.attachmentsOf(recordId), created]); }) .catch((err) => { - this.swal.error('Error', this.httpError.message(err, 'No se pudo adjuntar la imagen.')); + this.swal.error( + this.i18n.t('common.error'), + this.httpError.message(err, this.i18n.t('summary.attachError')), + ); }) .finally(() => this.uploadingRecordId.set(null)); } removeAttachment(recordId: string, attachment: MaintenanceAttachment): void { this.swal - .confirm('Eliminar adjunto', '¿Eliminar esta imagen del registro?', 'Eliminar', 'Cancelar') + .confirm( + this.i18n.t('summary.removeAttachmentTitle'), + this.i18n.t('summary.removeAttachmentText'), + this.i18n.t('summary.delete'), + this.i18n.t('common.cancel'), + ) .then((result) => { if (!result.isConfirmed) return; @@ -204,8 +216,8 @@ export class MotorcycleSummaryComponent implements OnInit, OnDestroy { }, error: (err) => { this.swal.error( - 'Error', - this.httpError.message(err, 'No se pudo eliminar el adjunto.'), + this.i18n.t('common.error'), + this.httpError.message(err, this.i18n.t('summary.removeAttachmentError')), ); }, }); @@ -258,8 +270,8 @@ export class MotorcycleSummaryComponent implements OnInit, OnDestroy { error: (err) => { this.isExporting.set(false); this.swal.error( - 'Error', - this.httpError.message(err, 'No se pudo generar el libro de mantenimiento.'), + this.i18n.t('common.error'), + this.httpError.message(err, this.i18n.t('summary.bookError')), ); }, }); @@ -280,7 +292,7 @@ export class MotorcycleSummaryComponent implements OnInit, OnDestroy { if (!id || this.isSubmittingKm()) return; if (this.editableKm() < this.currentKm()) { - this.swal.error('¡Error!', 'No puede agregar un Kilometraje inferior al actual.'); + this.swal.error(this.i18n.t('common.error'), this.i18n.t('summary.kmLowerError')); return; } @@ -288,15 +300,17 @@ export class MotorcycleSummaryComponent implements OnInit, OnDestroy { this.motorcyclesService.addKmHistory(id, this.editableKm()).subscribe({ next: () => { this.closeEditKmModal(); - this.swal.success('¡Exito!.', 'Se ha actualizado el Km de su motocicleta.').then(() => { - this.reloadData(); - }); + this.swal + .success(this.i18n.t('common.success'), this.i18n.t('summary.kmUpdated')) + .then(() => { + this.reloadData(); + }); }, error: (err) => { this.isSubmittingKm.set(false); this.swal.error( - 'Error', - this.httpError.message(err, 'No se pudo actualizar el kilometraje.'), + this.i18n.t('common.error'), + this.httpError.message(err, this.i18n.t('summary.kmUpdateError')), ); }, }); @@ -308,10 +322,10 @@ export class MotorcycleSummaryComponent implements OnInit, OnDestroy { this.swal .confirm( - 'Confirmar reversión', - 'Esto revertirá únicamente el último cambio de kilometraje.', - 'Revertir', - 'Cancelar', + this.i18n.t('summary.rollbackConfirmTitle'), + this.i18n.t('summary.rollbackConfirmText'), + this.i18n.t('summary.rollbackConfirmOk'), + this.i18n.t('common.cancel'), ) .then((result) => { if (!result.isConfirmed) return; @@ -320,15 +334,15 @@ export class MotorcycleSummaryComponent implements OnInit, OnDestroy { this.motorcyclesService.rollbackLastKm(id, this.currentKm()).subscribe({ next: () => { this.swal - .success('¡Éxito!', 'Se revirtió el último cambio de kilometraje.') + .success(this.i18n.t('common.success'), this.i18n.t('summary.kmRolledBack')) .then(() => { this.reloadData(); }); }, error: (err) => { this.swal.error( - 'Error', - this.httpError.message(err, 'No se pudo revertir el kilometraje.'), + this.i18n.t('common.error'), + this.httpError.message(err, this.i18n.t('summary.kmRollbackError')), ); }, complete: () => { diff --git a/bikontrol-web/src/app/modules/dashboard/pages/motorcycles/register-maintenance-record/register-maintenance-record.component.html b/bikontrol-web/src/app/modules/dashboard/pages/motorcycles/register-maintenance-record/register-maintenance-record.component.html index 7b35484..55e0ec6 100644 --- a/bikontrol-web/src/app/modules/dashboard/pages/motorcycles/register-maintenance-record/register-maintenance-record.component.html +++ b/bikontrol-web/src/app/modules/dashboard/pages/motorcycles/register-maintenance-record/register-maintenance-record.component.html @@ -1,15 +1,17 @@
-

Registrar mantenimiento

+

{{ 'record.title' | t }}

- + Registrar mantenimiento @if (selectedMaintenance()?.trackingType === 'Km') {
- + Registrar mantenimiento class="w-full rounded-lg p-2 bg-gray-900 text-white border border-gray-700" />

- Km actual: {{ currentKm() }} | Último de este mantenimiento: - {{ lastMaintenanceKm() ?? 'Sin registro' }} + {{ + 'record.currentKmHint' + | t: { km: currentKm(), last: lastMaintenanceKm() ?? ('record.noRecord' | t) } + }}

}
- + Registrar mantenimiento placeholder="0.00" class="w-full rounded-lg p-2 bg-gray-900 text-white border border-gray-700" /> -

- Sirve para las estadísticas de gasto. Déjalo vacío si no aplica. -

+

{{ 'record.costHint' | t }}

diff --git a/bikontrol-web/src/app/modules/dashboard/pages/motorcycles/register-maintenance-record/register-maintenance-record.component.ts b/bikontrol-web/src/app/modules/dashboard/pages/motorcycles/register-maintenance-record/register-maintenance-record.component.ts index 08c7822..fb46bc4 100644 --- a/bikontrol-web/src/app/modules/dashboard/pages/motorcycles/register-maintenance-record/register-maintenance-record.component.ts +++ b/bikontrol-web/src/app/modules/dashboard/pages/motorcycles/register-maintenance-record/register-maintenance-record.component.ts @@ -18,9 +18,12 @@ import { MotorcyclesService } from '../../../service/motorcycles.service'; import { SwalService } from '../../../../../shared/services/swal.service'; import { HttpErrorService } from '../../../../../shared/services/http-error.service'; +import { TranslatePipe } from '../../../../../shared/i18n/translate.pipe'; +import { I18nService } from '../../../../../shared/i18n/i18n.service'; + @Component({ selector: 'app-register-maintenance-record', - imports: [ReactiveFormsModule], + imports: [ReactiveFormsModule, TranslatePipe], templateUrl: './register-maintenance-record.component.html', changeDetection: ChangeDetectionStrategy.OnPush, styleUrl: './register-maintenance-record.component.scss', @@ -33,6 +36,7 @@ export class RegisterMaintenanceRecordComponent implements OnInit, OnDestroy { private motorcyclesService = inject(MotorcyclesService); private swal = inject(SwalService); private httpError = inject(HttpErrorService); + private i18n = inject(I18nService); readonly motorcycleId = signal(''); readonly maintenances = signal([]); @@ -95,7 +99,7 @@ export class RegisterMaintenanceRecordComponent implements OnInit, OnDestroy { this.maintenances.set(list); }, error: () => { - this.swal.error('Error', 'No se pudo cargar los mantenimientos de la moto.'); + this.swal.error(this.i18n.t('common.error'), this.i18n.t('record.loadError')); }, }); } @@ -139,7 +143,7 @@ export class RegisterMaintenanceRecordComponent implements OnInit, OnDestroy { const today = new Date(this.getTodayDate()); if (performedAt > today) { - this.swal.warning('Error', 'No puedes agregar mantenimientos posteriores al dia de hoy'); + this.swal.warning(this.i18n.t('common.error'), this.i18n.t('record.futureDate')); return; } @@ -151,7 +155,7 @@ export class RegisterMaintenanceRecordComponent implements OnInit, OnDestroy { performedKm != null && performedKm < lastKm ) { - this.swal.warning('Error', 'No puedes agregar mantenimiento anterior al ultimo'); + this.swal.warning(this.i18n.t('common.error'), this.i18n.t('record.beforeLast')); return; } @@ -167,7 +171,7 @@ export class RegisterMaintenanceRecordComponent implements OnInit, OnDestroy { this.maintenanceService.registerMaintenanceRecord(payload).subscribe({ next: () => { this.isSubmitting.set(false); - this.swal.success('¡Éxito!', 'Se registró el mantenimiento correctamente.').then(() => { + this.swal.success(this.i18n.t('common.success'), this.i18n.t('record.saved')).then(() => { this.router.navigate(['/dashboard/motorcycles/summary'], { queryParams: { motorcycleId: this.motorcycleId() }, }); @@ -176,8 +180,8 @@ export class RegisterMaintenanceRecordComponent implements OnInit, OnDestroy { error: (err) => { this.isSubmitting.set(false); this.swal.error( - 'Error', - this.httpError.message(err, 'No se pudo registrar el mantenimiento.'), + this.i18n.t('common.error'), + this.httpError.message(err, this.i18n.t('record.saveError')), ); }, }); diff --git a/bikontrol-web/src/app/modules/dashboard/pages/motorcycles/save-motorcycle/save-motorcycle.component.html b/bikontrol-web/src/app/modules/dashboard/pages/motorcycles/save-motorcycle/save-motorcycle.component.html index ef3a694..63f9caa 100644 --- a/bikontrol-web/src/app/modules/dashboard/pages/motorcycles/save-motorcycle/save-motorcycle.component.html +++ b/bikontrol-web/src/app/modules/dashboard/pages/motorcycles/save-motorcycle/save-motorcycle.component.html @@ -1,46 +1,52 @@

- {{ isEditMode() ? 'Editar Motocicleta' : 'Registrar Motocicleta' }} + {{ (isEditMode() ? 'motoForm.editTitle' : 'motoForm.createTitle') | t }}

- + @if (hasError('name', 'required')) { - Campo requerido + {{ 'motoForm.required' | t }} } @if (hasError('name', 'minlength')) { - Mínimo 2 caracteres + {{ 'motoForm.nameMin' | t }} }
- + @if (hasError('brand', 'required')) { - Campo requerido + {{ 'motoForm.required' | t }} }
- + class="w-full border border-gray-300 rounded-lg p-2 focus:ring-2 focus:ring-primary focus:outline-none" /> @if (hasError('year', 'required')) { - Campo requerido - } - @if (hasError('year', 'min')) { - Debe ser mayor o igual a 1950 + {{ 'motoForm.required' | t }} } - @if (hasError('year', 'max')) { + @if (hasError('year', 'min') || hasError('year', 'max')) { - Debe ser menor o igual a {{ currentYear + 1 }} + {{ 'motoForm.yearRange' | t: { max: currentYear + 1 } }} }
- + @if (isEditMode()) { - - Km actual (no editable, se gestiona desde el resumen) - + {{ 'motoForm.kmCurrentHint' | t }} } @if (hasError('km', 'required')) { - Campo requerido + {{ 'motoForm.required' | t }} } @if (hasError('km', 'min')) { - Debe ser positivo + {{ 'motoForm.kmPositive' | t }} } @if (hasError('km', 'max')) { - No puede superar 1,000,000 km + {{ 'motoForm.kmMax' | t }} }
- + @if (hasError('nickname', 'required')) { - Campo requerido + {{ 'motoForm.required' | t }} }
@@ -105,7 +110,7 @@

{{ 'motoForm.displacement' | t }} * class="w-full border border-gray-300 rounded-lg p-2 focus:ring-2 focus:ring-primary focus:outline-none" /> @if (hasError('displacement', 'required')) { - Campo requerido + {{ 'motoForm.required' | t }} } @if (hasError('displacement', 'min')) { - Debe ser positivo + {{ 'motoForm.displacementPositive' | t }} } @if (hasError('displacement', 'max')) { - No puede superar 2300 cc + {{ 'motoForm.displacementMax' | t }} }
- + @if (hasError('plate', 'required')) { - Campo requerido + {{ 'motoForm.required' | t }} }
@@ -143,12 +150,15 @@

Preview @@ -158,7 +168,7 @@

(click)="removeImage()" class="text-sm text-gray-500 hover:underline mt-1" > - Quitar imagen + {{ 'motoForm.removeImage' | t }} }

@@ -170,11 +180,8 @@

class="w-full bg-primary text-white py-2 rounded-lg hover:bg-primary-dark transition-colors" > {{ - isSubmitting() - ? 'Guardando...' - : isEditMode() - ? 'Actualizar Motocicleta' - : 'Guardar Motocicleta' + (isSubmitting() ? 'motoForm.saving' : isEditMode() ? 'motoForm.update' : 'motoForm.save') + | t }} diff --git a/bikontrol-web/src/app/modules/dashboard/pages/motorcycles/save-motorcycle/save-motorcycle.component.ts b/bikontrol-web/src/app/modules/dashboard/pages/motorcycles/save-motorcycle/save-motorcycle.component.ts index 6ea0573..f646f89 100644 --- a/bikontrol-web/src/app/modules/dashboard/pages/motorcycles/save-motorcycle/save-motorcycle.component.ts +++ b/bikontrol-web/src/app/modules/dashboard/pages/motorcycles/save-motorcycle/save-motorcycle.component.ts @@ -19,9 +19,12 @@ import { hasError as formHasError } from '../../../../../shared/utils/form.utils const PLACEHOLDER_IMAGE = '/assets/images/defaults/motorcycle-placeholder.webp'; +import { TranslatePipe } from '../../../../../shared/i18n/translate.pipe'; +import { I18nService } from '../../../../../shared/i18n/i18n.service'; + @Component({ selector: 'app-save-motorcycle', - imports: [ReactiveFormsModule], + imports: [ReactiveFormsModule, TranslatePipe], templateUrl: './save-motorcycle.component.html', changeDetection: ChangeDetectionStrategy.OnPush, styleUrl: './save-motorcycle.component.scss', @@ -34,6 +37,7 @@ export class SaveMotorcycleComponent implements OnInit, OnDestroy { private swal = inject(SwalService); private httpError = inject(HttpErrorService); private imageService = inject(ImageService); + private i18n = inject(I18nService); motorcycleForm: FormGroup; readonly isSubmitting = signal(false); @@ -88,7 +92,10 @@ export class SaveMotorcycleComponent implements OnInit, OnDestroy { } }, error: (err) => { - this.swal.error('Error', this.httpError.message(err, 'No se pudo cargar la motocicleta.')); + this.swal.error( + this.i18n.t('common.error'), + this.httpError.message(err, this.i18n.t('saveMotorcycle.loadError')), + ); }, }); } @@ -120,12 +127,15 @@ export class SaveMotorcycleComponent implements OnInit, OnDestroy { return; } if (!file.type.startsWith('image/')) { - this.swal.warning('Archivo inválido', 'Selecciona un archivo de imagen válido.'); + this.swal.warning(this.i18n.t('common.invalidFile'), this.i18n.t('common.selectValidImage')); input.value = ''; return; } if (file.size > 2 * 1024 * 1024) { - this.swal.warning('Archivo muy grande', 'La imagen no puede superar 2 MB.'); + this.swal.warning( + this.i18n.t('common.imageTooLarge'), + this.i18n.t('common.imageTooLargeText'), + ); input.value = ''; return; } @@ -137,7 +147,7 @@ export class SaveMotorcycleComponent implements OnInit, OnDestroy { this.previewSrc.set(resized); }) .catch(() => { - this.swal.warning('Archivo inválido', 'No se pudo leer la imagen seleccionada.'); + this.swal.warning(this.i18n.t('common.invalidFile'), this.i18n.t('common.imageReadError')); input.value = ''; }); } @@ -150,7 +160,10 @@ export class SaveMotorcycleComponent implements OnInit, OnDestroy { onSubmit(): void { if (this.motorcycleForm.invalid) { this.motorcycleForm.markAllAsTouched(); - this.swal.warning('Formulario incompleto', 'Por favor completa todos los campos requeridos.'); + this.swal.warning( + this.i18n.t('common.formIncomplete'), + this.i18n.t('common.completeRequired'), + ); return; } @@ -167,12 +180,15 @@ export class SaveMotorcycleComponent implements OnInit, OnDestroy { next: () => { this.isSubmitting.set(false); this.swal - .success('¡Éxito!', 'Motocicleta agregada correctamente.') + .success(this.i18n.t('common.success'), this.i18n.t('saveMotorcycle.added')) .then(() => this.router.navigate(['/dashboard'])); }, error: (err) => { this.isSubmitting.set(false); - this.swal.error('Error', this.httpError.message(err, 'No se pudo agregar la motocicleta.')); + this.swal.error( + this.i18n.t('common.error'), + this.httpError.message(err, this.i18n.t('saveMotorcycle.addError')), + ); }, }); } @@ -182,14 +198,14 @@ export class SaveMotorcycleComponent implements OnInit, OnDestroy { next: () => { this.isSubmitting.set(false); this.swal - .success('¡Éxito!', 'Motocicleta actualizada correctamente.') + .success(this.i18n.t('common.success'), this.i18n.t('saveMotorcycle.updated')) .then(() => this.router.navigate(['/dashboard'])); }, error: (err) => { this.isSubmitting.set(false); this.swal.error( - 'Error', - this.httpError.message(err, 'No se pudo actualizar la motocicleta.'), + this.i18n.t('common.error'), + this.httpError.message(err, this.i18n.t('saveMotorcycle.updateError')), ); }, }); diff --git a/bikontrol-web/src/app/shared/i18n/i18n.service.ts b/bikontrol-web/src/app/shared/i18n/i18n.service.ts index 2a31e5f..503342c 100644 --- a/bikontrol-web/src/app/shared/i18n/i18n.service.ts +++ b/bikontrol-web/src/app/shared/i18n/i18n.service.ts @@ -32,7 +32,7 @@ export class I18nService { } } - t(key: string, params?: Record): string { + t(key: string, params?: Record): string { let value = DICTIONARIES[this.current()][key] ?? es[key] ?? key; if (params) { for (const [name, replacement] of Object.entries(params)) { diff --git a/bikontrol-web/src/app/shared/i18n/translate.pipe.ts b/bikontrol-web/src/app/shared/i18n/translate.pipe.ts index a68f649..bdea455 100644 --- a/bikontrol-web/src/app/shared/i18n/translate.pipe.ts +++ b/bikontrol-web/src/app/shared/i18n/translate.pipe.ts @@ -9,7 +9,7 @@ import { I18nService } from './i18n.service'; export class TranslatePipe implements PipeTransform { private readonly i18n = inject(I18nService); - transform(key: string, params?: Record): string { + transform(key: string, params?: Record): string { return this.i18n.t(key, params); } } diff --git a/bikontrol-web/src/app/shared/i18n/translations/en.ts b/bikontrol-web/src/app/shared/i18n/translations/en.ts index 1d79888..8899e24 100644 --- a/bikontrol-web/src/app/shared/i18n/translations/en.ts +++ b/bikontrol-web/src/app/shared/i18n/translations/en.ts @@ -109,4 +109,143 @@ export const en: Record = { 'nav.home': 'Home', 'nav.statistics': 'Statistics', 'nav.profile': 'Profile', + + // Dashboard shell + 'dash.demoBanner': 'You are viewing the demo in read-only mode — changes are disabled.', + + // Home + 'home.welcome': 'Welcome back', + 'home.subtitle': 'Your motorcycles', + 'home.emptyTitle': 'Welcome!', + 'home.emptyText': 'Add your motorcycle and pick a maintenance plan in a couple of minutes.', + 'home.start': 'Get started', + 'home.addMotorcycle': '+ Add motorcycle', + + // Motorcycle card + 'motoCard.noNickname': 'No nickname', + 'motoCard.nickname': 'Nickname: {name}', + 'motoCard.edit': 'Edit', + 'motoCard.delete': 'Delete', + + // Save / edit motorcycle + 'motoForm.createTitle': 'Add motorcycle', + 'motoForm.editTitle': 'Edit motorcycle', + 'motoForm.name': 'Name', + 'motoForm.brand': 'Brand', + 'motoForm.year': 'Model year', + 'motoForm.km': 'Mileage', + 'motoForm.nickname': 'Nickname', + 'motoForm.displacement': 'Displacement (cc)', + 'motoForm.plate': 'Plate', + 'motoForm.required': 'Required field', + 'motoForm.nameMin': 'At least 2 characters', + 'motoForm.yearRange': 'Must be between 1950 and {max}', + 'motoForm.kmPositive': 'Must be positive', + 'motoForm.kmMax': 'Cannot exceed 1,000,000 km', + 'motoForm.displacementPositive': 'Must be positive', + 'motoForm.displacementMax': 'Cannot exceed 2300 cc', + 'motoForm.uploadImage': 'Upload image', + 'motoForm.changeImage': 'Change image', + 'motoForm.removeImage': 'Remove image', + 'motoForm.kmCurrentHint': 'Current km (read-only, managed from the summary)', + 'motoForm.saving': 'Saving...', + 'motoForm.save': 'Save motorcycle', + 'motoForm.update': 'Update motorcycle', + 'motoForm.previewAlt': 'Preview', + + // Motorcycle summary + 'summary.currentKm': 'Current mileage', + 'summary.editKm': 'Edit mileage', + 'summary.rollbackHint': 'Got the mileage wrong? Roll it back here.', + 'summary.exportPdf': 'Export book (PDF)', + 'summary.exportCsv': 'Export (CSV)', + 'summary.upcoming': 'Upcoming maintenance', + 'summary.registerMaintenance': '+ Log maintenance', + 'summary.overdueKm': 'Overdue by {n} km', + 'summary.remainingKm': '{n} km left', + 'summary.overdueDays': 'Overdue by {n} day(s)', + 'summary.remainingDays': '{n} day(s) left', + 'summary.lastPerformed': 'Last maintenance: {date}', + 'summary.noMaintenances': 'You have not added maintenance for this motorcycle yet.', + 'summary.addMaintenances': '+ Add maintenance', + 'summary.records': 'Maintenance log', + 'summary.recordKm': 'Km: {n}', + 'summary.recordCost': 'Cost: {c}', + 'summary.photos': 'Photos and documents', + 'summary.uploading': 'Uploading…', + 'summary.attach': '+ Attach', + 'summary.viewAttachment': 'View attachment', + 'summary.removeAttachment': 'Remove attachment', + 'summary.attachment': 'Attachment', + 'summary.noRecords': 'No maintenance records for this motorcycle yet.', + 'summary.loadingRecords': 'Loading records…', + 'summary.notFound': 'Motorcycle information not found.', + 'summary.redirecting': 'You will be redirected home shortly.', + 'summary.editKmTitle': 'Enter the new mileage for your motorcycle', + 'summary.kmLabel': 'Mileage', + 'summary.cancel': 'Cancel', + 'summary.accept': 'Accept', + 'summary.closeAttachment': 'Close attachment', + 'summary.close': 'Close', + + // Register maintenance record + 'record.title': 'Log maintenance', + 'record.maintenance': 'Maintenance', + 'record.selectMaintenance': 'Select a maintenance item', + 'record.date': 'Date', + 'record.km': 'Mileage', + 'record.currentKmHint': 'Current km: {km} | Last for this item: {last}', + 'record.noRecord': 'No record', + 'record.cost': 'Cost (optional)', + 'record.costHint': 'Feeds the spending statistics. Leave it empty if it does not apply.', + 'record.submit': 'Log maintenance', + 'record.submitting': 'Saving...', + + // Dialog labels / errors + 'common.error': 'Error', + 'common.success': 'Success!', + 'common.cancel': 'Cancel', + 'common.yesDelete': 'Yes, delete', + 'common.formIncomplete': 'Incomplete form', + 'common.completeRequired': 'Please complete all required fields.', + 'common.invalidFile': 'Invalid file', + 'common.selectValidImage': 'Select a valid image file.', + 'common.imageTooLarge': 'File too large', + 'common.imageTooLargeText': 'The image cannot exceed 2 MB.', + 'common.imageReadError': 'Could not read the selected image.', + + 'motoCard.deletedTitle': 'Deleted!', + 'motoCard.deletedText': 'The motorcycle was deleted.', + 'motoCard.deleteError': 'Could not delete the motorcycle.', + 'motoCard.deleteConfirmTitle': 'Are you sure?', + 'motoCard.deleteConfirmText': 'This will permanently delete "{name}".', + + 'saveMotorcycle.loadError': 'Could not load the motorcycle.', + 'saveMotorcycle.added': 'Motorcycle added.', + 'saveMotorcycle.addError': 'Could not add the motorcycle.', + 'saveMotorcycle.updated': 'Motorcycle updated.', + 'saveMotorcycle.updateError': 'Could not update the motorcycle.', + + 'record.loadError': 'Could not load the motorcycle maintenance.', + 'record.futureDate': 'You cannot add maintenance dated in the future', + 'record.beforeLast': 'You cannot add maintenance earlier than the last one', + 'record.saved': 'Maintenance logged.', + 'record.saveError': 'Could not log the maintenance.', + + 'summary.upcomingError': 'Could not load upcoming maintenance.', + 'summary.recordsError': 'Could not load the maintenance records.', + 'summary.attachError': 'Could not attach the image.', + 'summary.removeAttachmentError': 'Could not remove the attachment.', + 'summary.bookError': 'Could not generate the maintenance book.', + 'summary.kmLowerError': 'You cannot set a mileage lower than the current one.', + 'summary.kmUpdated': 'Your motorcycle mileage was updated.', + 'summary.kmUpdateError': 'Could not update the mileage.', + 'summary.kmRollbackError': 'Could not roll back the mileage.', + 'summary.removeAttachmentTitle': 'Remove attachment', + 'summary.removeAttachmentText': 'Remove this image from the record?', + 'summary.delete': 'Delete', + 'summary.rollbackConfirmTitle': 'Confirm rollback', + 'summary.rollbackConfirmText': 'This will only revert the last mileage change.', + 'summary.rollbackConfirmOk': 'Roll back', + 'summary.kmRolledBack': 'The last mileage change was rolled back.', }; diff --git a/bikontrol-web/src/app/shared/i18n/translations/es.ts b/bikontrol-web/src/app/shared/i18n/translations/es.ts index 605d02c..2a6df88 100644 --- a/bikontrol-web/src/app/shared/i18n/translations/es.ts +++ b/bikontrol-web/src/app/shared/i18n/translations/es.ts @@ -112,4 +112,144 @@ export const es: Record = { 'nav.home': 'Inicio', 'nav.statistics': 'Estadísticas', 'nav.profile': 'Perfil', + + // Dashboard shell + 'dash.demoBanner': + 'Estás viendo la demo en modo solo lectura — los cambios están deshabilitados.', + + // Home + 'home.welcome': 'Bienvenido de nuevo', + 'home.subtitle': 'Tus motocicletas', + 'home.emptyTitle': '¡Te damos la bienvenida!', + 'home.emptyText': 'Agregá tu moto y elegí un plan de mantenimiento en un par de minutos.', + 'home.start': 'Comenzar', + 'home.addMotorcycle': '+ Agregar motocicleta', + + // Motorcycle card + 'motoCard.noNickname': 'Sin apodo', + 'motoCard.nickname': 'Apodo: {name}', + 'motoCard.edit': 'Editar', + 'motoCard.delete': 'Borrar', + + // Save / edit motorcycle + 'motoForm.createTitle': 'Registrar Motocicleta', + 'motoForm.editTitle': 'Editar Motocicleta', + 'motoForm.name': 'Nombre', + 'motoForm.brand': 'Marca', + 'motoForm.year': 'Modelo', + 'motoForm.km': 'Kilometraje', + 'motoForm.nickname': 'Apodo', + 'motoForm.displacement': 'Cilindraje (cc)', + 'motoForm.plate': 'Placa', + 'motoForm.required': 'Campo requerido', + 'motoForm.nameMin': 'Mínimo 2 caracteres', + 'motoForm.yearRange': 'Debe ser mayor o igual a 1950 y menor o igual a {max}', + 'motoForm.kmPositive': 'Debe ser positivo', + 'motoForm.kmMax': 'No puede superar 1,000,000 km', + 'motoForm.displacementPositive': 'Debe ser positivo', + 'motoForm.displacementMax': 'No puede superar 2300 cc', + 'motoForm.uploadImage': 'Subir imagen', + 'motoForm.changeImage': 'Cambiar imagen', + 'motoForm.removeImage': 'Quitar imagen', + 'motoForm.kmCurrentHint': 'Km actual (no editable, se gestiona desde el resumen)', + 'motoForm.saving': 'Guardando...', + 'motoForm.save': 'Guardar Motocicleta', + 'motoForm.update': 'Actualizar Motocicleta', + 'motoForm.previewAlt': 'Vista previa', + + // Motorcycle summary + 'summary.currentKm': 'Kilometraje Actual', + 'summary.editKm': 'Editar kilometraje', + 'summary.rollbackHint': '¿Te equivocaste con el kilometraje? Reviértelo aquí.', + 'summary.exportPdf': 'Exportar libro (PDF)', + 'summary.exportCsv': 'Exportar (CSV)', + 'summary.upcoming': 'Proximos Mantenimientos', + 'summary.registerMaintenance': '+ Registrar mantenimiento', + 'summary.overdueKm': 'Vencido por {n} km', + 'summary.remainingKm': 'Faltan {n} km', + 'summary.overdueDays': 'Vencido por {n} día(s)', + 'summary.remainingDays': 'Faltan {n} día(s)', + 'summary.lastPerformed': 'Último mantenimiento: {date}', + 'summary.noMaintenances': 'Aún no has agregado mantenimientos para esta moto.', + 'summary.addMaintenances': '+ Agregar mantenimientos', + 'summary.records': 'Registro de mantenimientos', + 'summary.recordKm': 'Km: {n}', + 'summary.recordCost': 'Costo: {c}', + 'summary.photos': 'Fotos y documentos', + 'summary.uploading': 'Subiendo…', + 'summary.attach': '+ Adjuntar', + 'summary.viewAttachment': 'Ver adjunto', + 'summary.removeAttachment': 'Eliminar adjunto', + 'summary.attachment': 'Adjunto', + 'summary.noRecords': 'Aún no hay registros de mantenimientos para esta moto.', + 'summary.loadingRecords': 'Cargando registros…', + 'summary.notFound': 'No se encontró información de la motocicleta.', + 'summary.redirecting': 'Serás redirigido al inicio en breve.', + 'summary.editKmTitle': 'Ingrese el nuevo Kilometraje de su motocicleta', + 'summary.kmLabel': 'Kilometraje', + 'summary.cancel': 'Cancelar', + 'summary.accept': 'Aceptar', + 'summary.closeAttachment': 'Cerrar adjunto', + 'summary.close': 'Cerrar', + + // Register maintenance record + 'record.title': 'Registrar mantenimiento', + 'record.maintenance': 'Mantenimiento', + 'record.selectMaintenance': 'Selecciona un mantenimiento', + 'record.date': 'Fecha', + 'record.km': 'Kilometraje', + 'record.currentKmHint': 'Km actual: {km} | Último de este mantenimiento: {last}', + 'record.noRecord': 'Sin registro', + 'record.cost': 'Costo (opcional)', + 'record.costHint': 'Sirve para las estadísticas de gasto. Déjalo vacío si no aplica.', + 'record.submit': 'Registrar mantenimiento', + 'record.submitting': 'Guardando...', + + // Dialog labels / errors + 'common.error': 'Error', + 'common.success': '¡Éxito!', + 'common.cancel': 'Cancelar', + 'common.yesDelete': 'Sí, eliminar', + 'common.formIncomplete': 'Formulario incompleto', + 'common.completeRequired': 'Por favor completa todos los campos requeridos.', + 'common.invalidFile': 'Archivo inválido', + 'common.selectValidImage': 'Selecciona un archivo de imagen válido.', + 'common.imageTooLarge': 'Archivo muy grande', + 'common.imageTooLargeText': 'La imagen no puede superar 2 MB.', + 'common.imageReadError': 'No se pudo leer la imagen seleccionada.', + + 'motoCard.deletedTitle': '¡Eliminada!', + 'motoCard.deletedText': 'La motocicleta fue eliminada correctamente.', + 'motoCard.deleteError': 'No se pudo eliminar la motocicleta.', + 'motoCard.deleteConfirmTitle': '¿Estás seguro?', + 'motoCard.deleteConfirmText': 'Esto eliminará permanentemente "{name}".', + + 'saveMotorcycle.loadError': 'No se pudo cargar la motocicleta.', + 'saveMotorcycle.added': 'Motocicleta agregada correctamente.', + 'saveMotorcycle.addError': 'No se pudo agregar la motocicleta.', + 'saveMotorcycle.updated': 'Motocicleta actualizada correctamente.', + 'saveMotorcycle.updateError': 'No se pudo actualizar la motocicleta.', + + 'record.loadError': 'No se pudo cargar los mantenimientos de la moto.', + 'record.futureDate': 'No puedes agregar mantenimientos posteriores al dia de hoy', + 'record.beforeLast': 'No puedes agregar mantenimiento anterior al ultimo', + 'record.saved': 'Se registró el mantenimiento correctamente.', + 'record.saveError': 'No se pudo registrar el mantenimiento.', + + 'summary.upcomingError': 'No se pudieron cargar los mantenimientos próximos.', + 'summary.recordsError': 'No se pudieron cargar los registros de mantenimiento.', + 'summary.attachError': 'No se pudo adjuntar la imagen.', + 'summary.removeAttachmentError': 'No se pudo eliminar el adjunto.', + 'summary.bookError': 'No se pudo generar el libro de mantenimiento.', + 'summary.kmLowerError': 'No puede agregar un Kilometraje inferior al actual.', + 'summary.kmUpdated': 'Se ha actualizado el Km de su motocicleta.', + 'summary.kmUpdateError': 'No se pudo actualizar el kilometraje.', + 'summary.kmRollbackError': 'No se pudo revertir el kilometraje.', + 'summary.removeAttachmentTitle': 'Eliminar adjunto', + 'summary.removeAttachmentText': '¿Eliminar esta imagen del registro?', + 'summary.delete': 'Eliminar', + 'summary.rollbackConfirmTitle': 'Confirmar reversión', + 'summary.rollbackConfirmText': 'Esto revertirá únicamente el último cambio de kilometraje.', + 'summary.rollbackConfirmOk': 'Revertir', + 'summary.kmRolledBack': 'Se revirtió el último cambio de kilometraje.', }; diff --git a/docs/specs/i18n.md b/docs/specs/i18n.md index 1510a5b..2b59ccb 100644 --- a/docs/specs/i18n.md +++ b/docs/specs/i18n.md @@ -8,10 +8,10 @@ Runtime i18n with a minimal, dependency-free service (`shared/i18n`): - **`TranslatePipe`** (`{{ 'key' | t }}`) — impure on purpose so it re-renders on language change. - **`LanguageSwitcherComponent`** (ES/EN), currently in the landing header. -**Coverage is incremental.** Done: **landing**, **auth** (login, register, forgot/reset/confirm) -and the **app shell** (top/bottom nav, incl. the ES/EN switcher in the profile menu). Pending -(follow-up PR): dashboard pages (home, motorcycle summary, maintenance, statistics, profile) and the -legal/help long-form text. +**Coverage is incremental.** Done: **landing**, **auth** (login, register, forgot/reset/confirm), +the **app shell** (top/bottom nav + switcher in the profile menu) and the **motorcycle flow** +(home, card, add/edit, summary, log-record, incl. their dialogs). Pending (final PR): maintenance +page, save/follow maintenance, statistics, profile, and the legal/help long-form text. When adding user-facing text, add the key to **both** dictionaries; a missing key falls back to Spanish, then to the key itself (so untranslated strings remain visible, not blank).