diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml
index 8811a3b..76e6cc6 100644
--- a/.github/workflows/sonarcloud.yml
+++ b/.github/workflows/sonarcloud.yml
@@ -83,7 +83,14 @@ jobs:
# repetition is tool output, not something to factor out, so exclude
# the Migrations folder from copy-paste detection. Migration Up is
# still executed by the integration fixture and reviewed in the PR.
- "/d:sonar.cpd.exclusions=**/Migrations/**"
+ #
+ # Also excluded from copy-paste detection:
+ # - i18n dictionaries: `es` and `en` mirror each other by design
+ # (same keys, translated values), which CPD reads as duplication.
+ # - component templates: Tailwind markup (cards, form fields) is
+ # presentation, not logic; factoring it out adds indirection with
+ # no behavioural gain.
+ "/d:sonar.cpd.exclusions=**/Migrations/**,**/shared/i18n/translations/*.ts,**/*.html"
)
if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then
args+=(
diff --git a/bikontrol-web/src/app/modules/auth/auth-imports.ts b/bikontrol-web/src/app/modules/auth/auth-imports.ts
index 9eeb959..56aff85 100644
--- a/bikontrol-web/src/app/modules/auth/auth-imports.ts
+++ b/bikontrol-web/src/app/modules/auth/auth-imports.ts
@@ -1,5 +1,14 @@
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
+import { TranslatePipe } from '../../shared/i18n/translate.pipe';
+import { LanguageSwitcherComponent } from '../../shared/i18n/language-switcher.component';
-export const AUTH_IMPORTS = [CommonModule, FormsModule, ReactiveFormsModule, RouterModule];
+export const AUTH_IMPORTS = [
+ CommonModule,
+ FormsModule,
+ ReactiveFormsModule,
+ RouterModule,
+ TranslatePipe,
+ LanguageSwitcherComponent,
+];
diff --git a/bikontrol-web/src/app/modules/auth/pages/confirm-email/confirm-email.component.html b/bikontrol-web/src/app/modules/auth/pages/confirm-email/confirm-email.component.html
index 8678c5e..3a327a0 100644
--- a/bikontrol-web/src/app/modules/auth/pages/confirm-email/confirm-email.component.html
+++ b/bikontrol-web/src/app/modules/auth/pages/confirm-email/confirm-email.component.html
@@ -2,10 +2,10 @@

-
Confirmar correo
+
{{ 'auth.confirm.title' | t }}
@if (loading()) {
-
Confirmando tu correo...
+
{{ 'auth.confirm.loading' | t }}
} @else if (success()) {
{{ successMessage() }}
} @else {
@@ -17,7 +17,7 @@
Confirmar correo
routerLink="/login"
class="w-full py-2 bg-primary text-textlight rounded-lg hover:bg-accent transition-colors"
>
- Ir al inicio de sesión
+ {{ 'auth.confirm.goToLogin' | t }}
diff --git a/bikontrol-web/src/app/modules/auth/pages/confirm-email/confirm-email.component.spec.ts b/bikontrol-web/src/app/modules/auth/pages/confirm-email/confirm-email.component.spec.ts
index b4f00aa..8f98b1f 100644
--- a/bikontrol-web/src/app/modules/auth/pages/confirm-email/confirm-email.component.spec.ts
+++ b/bikontrol-web/src/app/modules/auth/pages/confirm-email/confirm-email.component.spec.ts
@@ -51,7 +51,7 @@ describe('ConfirmEmailComponent', () => {
expect(authServiceMock.confirmEmail).toHaveBeenCalledWith('user@example.com', 'token-abc');
expect(component.success()).toBe(true);
- expect(component.successMessage()).toBe('Correo confirmado.');
+ expect(component.successMessage()).toBe('Correo confirmado. Ya puedes iniciar sesión.');
expect(component.loading()).toBe(false);
});
diff --git a/bikontrol-web/src/app/modules/auth/pages/confirm-email/confirm-email.component.ts b/bikontrol-web/src/app/modules/auth/pages/confirm-email/confirm-email.component.ts
index 59a2010..fbb6bd9 100644
--- a/bikontrol-web/src/app/modules/auth/pages/confirm-email/confirm-email.component.ts
+++ b/bikontrol-web/src/app/modules/auth/pages/confirm-email/confirm-email.component.ts
@@ -2,10 +2,12 @@ import { Component, OnInit, ChangeDetectionStrategy, signal, inject } from '@ang
import { ActivatedRoute, RouterModule } from '@angular/router';
import { AuthService } from '../../services/auth.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-confirm-email',
- imports: [RouterModule],
+ imports: [RouterModule, TranslatePipe],
templateUrl: './confirm-email.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './confirm-email.component.scss',
@@ -14,6 +16,7 @@ export class ConfirmEmailComponent implements OnInit {
private route = inject(ActivatedRoute);
private authService = inject(AuthService);
private httpError = inject(HttpErrorService);
+ private i18n = inject(I18nService);
readonly loading = signal(true);
readonly success = signal(false);
@@ -26,15 +29,15 @@ export class ConfirmEmailComponent implements OnInit {
if (!token || !email) {
this.loading.set(false);
- this.errorMessage.set('El enlace de confirmación es inválido o está incompleto.');
+ this.errorMessage.set(this.i18n.t('auth.confirm.linkInvalid'));
return;
}
this.authService.confirmEmail(email, token).subscribe({
- next: (response) => {
+ next: () => {
this.loading.set(false);
this.success.set(true);
- this.successMessage.set(response.message);
+ this.successMessage.set(this.i18n.t('auth.confirm.success'));
},
error: (error) => {
this.loading.set(false);
diff --git a/bikontrol-web/src/app/modules/auth/pages/forgot-password/forgot-password.component.html b/bikontrol-web/src/app/modules/auth/pages/forgot-password/forgot-password.component.html
index 708e0dc..3d2802f 100644
--- a/bikontrol-web/src/app/modules/auth/pages/forgot-password/forgot-password.component.html
+++ b/bikontrol-web/src/app/modules/auth/pages/forgot-password/forgot-password.component.html
@@ -2,28 +2,26 @@

-
Recuperar contraseña
-
- Ingresa tu correo y te enviaremos un enlace para restablecer tu contraseña.
-
+
{{ 'auth.forgot.title' | t }}
+
{{ 'auth.forgot.subtitle' | t }}
diff --git a/bikontrol-web/src/app/modules/auth/pages/forgot-password/forgot-password.component.spec.ts b/bikontrol-web/src/app/modules/auth/pages/forgot-password/forgot-password.component.spec.ts
index 0a95ebe..63318ba 100644
--- a/bikontrol-web/src/app/modules/auth/pages/forgot-password/forgot-password.component.spec.ts
+++ b/bikontrol-web/src/app/modules/auth/pages/forgot-password/forgot-password.component.spec.ts
@@ -49,7 +49,9 @@ describe('ForgotPasswordComponent', () => {
component.onSubmit();
expect(authServiceMock.forgotPassword).toHaveBeenCalledWith('user@example.com');
- expect(component.successMessage()).toBe('Revisa tu correo.');
+ expect(component.successMessage()).toBe(
+ 'Si el correo está registrado, recibirás un enlace para restablecer tu contraseña.',
+ );
expect(component.errorMessage()).toBeNull();
});
diff --git a/bikontrol-web/src/app/modules/auth/pages/forgot-password/forgot-password.component.ts b/bikontrol-web/src/app/modules/auth/pages/forgot-password/forgot-password.component.ts
index 47dd33b..06c3363 100644
--- a/bikontrol-web/src/app/modules/auth/pages/forgot-password/forgot-password.component.ts
+++ b/bikontrol-web/src/app/modules/auth/pages/forgot-password/forgot-password.component.ts
@@ -10,10 +10,12 @@ import { RouterModule } from '@angular/router';
import { AuthService } from '../../services/auth.service';
import { HttpErrorService } from '../../../../shared/services/http-error.service';
import { isInvalid as formIsInvalid } from '../../../../shared/utils/form.utils';
+import { TranslatePipe } from '../../../../shared/i18n/translate.pipe';
+import { I18nService } from '../../../../shared/i18n/i18n.service';
@Component({
selector: 'app-forgot-password',
- imports: [FormsModule, ReactiveFormsModule, RouterModule],
+ imports: [FormsModule, ReactiveFormsModule, RouterModule, TranslatePipe],
templateUrl: './forgot-password.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './forgot-password.component.scss',
@@ -22,6 +24,7 @@ export class ForgotPasswordComponent {
private fb = inject(FormBuilder);
private authService = inject(AuthService);
private httpError = inject(HttpErrorService);
+ private i18n = inject(I18nService);
form: FormGroup;
readonly submitted = signal(false);
@@ -50,8 +53,8 @@ export class ForgotPasswordComponent {
if (this.form.invalid) return;
this.authService.forgotPassword(this.form.value.email).subscribe({
- next: (response) => {
- this.successMessage.set(response.message);
+ next: () => {
+ this.successMessage.set(this.i18n.t('auth.forgot.success'));
},
error: (error) => {
this.errorMessage.set(this.httpError.message(error));
diff --git a/bikontrol-web/src/app/modules/auth/pages/login/login.component.html b/bikontrol-web/src/app/modules/auth/pages/login/login.component.html
index 92e47ab..0965677 100644
--- a/bikontrol-web/src/app/modules/auth/pages/login/login.component.html
+++ b/bikontrol-web/src/app/modules/auth/pages/login/login.component.html
@@ -2,30 +2,32 @@

-
Bikontrol
+
{{ 'app.name' | t }}
diff --git a/bikontrol-web/src/app/modules/auth/pages/login/login.component.spec.ts b/bikontrol-web/src/app/modules/auth/pages/login/login.component.spec.ts
index dd9a7bd..a79f81a 100644
--- a/bikontrol-web/src/app/modules/auth/pages/login/login.component.spec.ts
+++ b/bikontrol-web/src/app/modules/auth/pages/login/login.component.spec.ts
@@ -132,7 +132,7 @@ describe('LoginComponent', () => {
component.onResendConfirmation();
expect(authServiceMock.resendConfirmation).toHaveBeenCalledWith('user@example.com');
- expect(component.resendMessage()).toBe('Enviado');
+ expect(component.resendMessage()).toBe('Te enviamos un nuevo enlace de confirmación.');
});
it('should log in to the demo and navigate when demo is enabled', () => {
diff --git a/bikontrol-web/src/app/modules/auth/pages/login/login.component.ts b/bikontrol-web/src/app/modules/auth/pages/login/login.component.ts
index c4ecaff..7c91d3d 100644
--- a/bikontrol-web/src/app/modules/auth/pages/login/login.component.ts
+++ b/bikontrol-web/src/app/modules/auth/pages/login/login.component.ts
@@ -5,6 +5,7 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AUTH_IMPORTS } from '../../auth-imports';
import { HttpErrorService } from '../../../../shared/services/http-error.service';
import { isInvalid as formIsInvalid } from '../../../../shared/utils/form.utils';
+import { I18nService } from '../../../../shared/i18n/i18n.service';
import { environment } from '@env/environment';
declare global {
@@ -25,6 +26,7 @@ export class LoginComponent implements AfterViewInit {
private authService = inject(AuthService);
private router = inject(Router);
private httpError = inject(HttpErrorService);
+ private i18n = inject(I18nService);
loginForm: FormGroup;
readonly submitted = signal(false);
@@ -93,9 +95,9 @@ export class LoginComponent implements AfterViewInit {
this.resendLoading.set(true);
this.resendMessage.set(null);
this.authService.resendConfirmation(email).subscribe({
- next: (response) => {
+ next: () => {
this.resendLoading.set(false);
- this.resendMessage.set(response.message);
+ this.resendMessage.set(this.i18n.t('auth.login.resendSent'));
},
error: (error) => {
this.resendLoading.set(false);
@@ -113,7 +115,7 @@ export class LoginComponent implements AfterViewInit {
next: () => this.router.navigate(['/dashboard']),
error: (error) => {
this.demoLoading.set(false);
- this.errorMessage.set(this.httpError.message(error, 'No se pudo iniciar la demo.'));
+ this.errorMessage.set(this.httpError.message(error, this.i18n.t('auth.login.demoError')));
},
});
}
@@ -148,7 +150,7 @@ export class LoginComponent implements AfterViewInit {
onGoogleCredential(response: { credential?: string }): void {
if (!response?.credential) {
- this.errorMessage.set('No se pudo obtener la credencial de Google.');
+ this.errorMessage.set(this.i18n.t('auth.login.googleError'));
return;
}
diff --git a/bikontrol-web/src/app/modules/auth/pages/register/register.component.html b/bikontrol-web/src/app/modules/auth/pages/register/register.component.html
index e7fa43c..94726a2 100644
--- a/bikontrol-web/src/app/modules/auth/pages/register/register.component.html
+++ b/bikontrol-web/src/app/modules/auth/pages/register/register.component.html
@@ -2,13 +2,12 @@

-
Crear cuenta
+
+ {{ 'auth.register.title' | t }}
+
@if (confirmationSent()) {
-
- Te enviamos un correo de confirmación. Revisá tu bandeja (y la carpeta de spam) y hacé clic
- en el enlace para activar tu cuenta.
-
+
{{ 'auth.register.confirmSent' | t }}
@if (resendMessage()) {
@@ -35,49 +34,51 @@
Crear cuenta
routerLink="/login"
class="w-full mt-6 py-2 bg-primary text-textlight rounded-lg hover:bg-accent transition-colors"
>
- Ir al inicio de sesión
+ {{ 'auth.confirm.goToLogin' | t }}
} @else {
}
-
- Ayuda
- ·
- Términos
- ·
- Privacidad
-
+
diff --git a/bikontrol-web/src/app/modules/auth/pages/register/register.component.spec.ts b/bikontrol-web/src/app/modules/auth/pages/register/register.component.spec.ts
index bf54a21..375c1a5 100644
--- a/bikontrol-web/src/app/modules/auth/pages/register/register.component.spec.ts
+++ b/bikontrol-web/src/app/modules/auth/pages/register/register.component.spec.ts
@@ -135,7 +135,7 @@ describe('RegisterComponent', () => {
component.onResendConfirmation();
expect(authServiceMock.resendConfirmation).toHaveBeenCalledWith('user@example.com');
- expect(component.resendMessage()).toBe('Enviado');
+ expect(component.resendMessage()).toBe('Te enviamos un nuevo enlace de confirmación.');
});
it('should use the generic fallback message when the backend response is empty', () => {
diff --git a/bikontrol-web/src/app/modules/auth/pages/register/register.component.ts b/bikontrol-web/src/app/modules/auth/pages/register/register.component.ts
index 3418dff..4f0b448 100644
--- a/bikontrol-web/src/app/modules/auth/pages/register/register.component.ts
+++ b/bikontrol-web/src/app/modules/auth/pages/register/register.component.ts
@@ -5,6 +5,7 @@ import { Router } from '@angular/router';
import { AUTH_IMPORTS } from '../../auth-imports';
import { HttpErrorService } from '../../../../shared/services/http-error.service';
import { isInvalid as formIsInvalid } from '../../../../shared/utils/form.utils';
+import { I18nService } from '../../../../shared/i18n/i18n.service';
@Component({
selector: 'app-register',
@@ -18,6 +19,7 @@ export class RegisterComponent {
private authService = inject(AuthService);
private router = inject(Router);
private httpError = inject(HttpErrorService);
+ private i18n = inject(I18nService);
registerForm: FormGroup;
readonly submitted = signal(false);
@@ -87,9 +89,9 @@ export class RegisterComponent {
this.resendLoading.set(true);
this.resendMessage.set(null);
this.authService.resendConfirmation(this.registeredEmail).subscribe({
- next: (response) => {
+ next: () => {
this.resendLoading.set(false);
- this.resendMessage.set(response.message);
+ this.resendMessage.set(this.i18n.t('auth.login.resendSent'));
},
error: (error) => {
this.resendLoading.set(false);
diff --git a/bikontrol-web/src/app/modules/auth/pages/reset-password/reset-password.component.html b/bikontrol-web/src/app/modules/auth/pages/reset-password/reset-password.component.html
index 0fcbc46..e10f7f1 100644
--- a/bikontrol-web/src/app/modules/auth/pages/reset-password/reset-password.component.html
+++ b/bikontrol-web/src/app/modules/auth/pages/reset-password/reset-password.component.html
@@ -2,19 +2,19 @@

-
Nueva contraseña
+
{{ 'auth.reset.title' | t }}
@if (linkInvalid()) {
- El enlace de recuperación es inválido o está incompleto. Solicita uno nuevo.
+ {{ 'auth.reset.linkInvalid' | t }}
}