iatoolkit 0.63.1__py3-none-any.whl → 0.67.0__py3-none-any.whl

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.

Potentially problematic release.


This version of iatoolkit might be problematic. Click here for more details.

Files changed (78) hide show
  1. iatoolkit/__init__.py +2 -0
  2. iatoolkit/base_company.py +1 -20
  3. iatoolkit/common/routes.py +11 -2
  4. iatoolkit/common/session_manager.py +2 -0
  5. iatoolkit/common/util.py +17 -0
  6. iatoolkit/company_registry.py +1 -2
  7. iatoolkit/iatoolkit.py +41 -5
  8. iatoolkit/locales/en.yaml +167 -0
  9. iatoolkit/locales/es.yaml +163 -0
  10. iatoolkit/repositories/database_manager.py +3 -3
  11. iatoolkit/repositories/document_repo.py +1 -1
  12. iatoolkit/repositories/models.py +2 -3
  13. iatoolkit/repositories/profile_repo.py +0 -4
  14. iatoolkit/services/auth_service.py +14 -9
  15. iatoolkit/services/branding_service.py +32 -22
  16. iatoolkit/services/configuration_service.py +140 -0
  17. iatoolkit/services/dispatcher_service.py +20 -18
  18. iatoolkit/services/document_service.py +5 -2
  19. iatoolkit/services/excel_service.py +15 -11
  20. iatoolkit/services/file_processor_service.py +4 -12
  21. iatoolkit/services/history_service.py +8 -7
  22. iatoolkit/services/i18n_service.py +104 -0
  23. iatoolkit/services/jwt_service.py +7 -9
  24. iatoolkit/services/language_service.py +79 -0
  25. iatoolkit/services/load_documents_service.py +4 -4
  26. iatoolkit/services/mail_service.py +9 -4
  27. iatoolkit/services/onboarding_service.py +10 -4
  28. iatoolkit/services/profile_service.py +58 -38
  29. iatoolkit/services/prompt_manager_service.py +20 -16
  30. iatoolkit/services/query_service.py +15 -14
  31. iatoolkit/services/sql_service.py +6 -2
  32. iatoolkit/services/user_feedback_service.py +16 -14
  33. iatoolkit/static/js/chat_feedback_button.js +57 -87
  34. iatoolkit/static/js/chat_help_content.js +124 -0
  35. iatoolkit/static/js/chat_history_button.js +48 -65
  36. iatoolkit/static/js/chat_main.js +27 -24
  37. iatoolkit/static/js/chat_reload_button.js +28 -45
  38. iatoolkit/static/styles/chat_iatoolkit.css +223 -315
  39. iatoolkit/static/styles/chat_modal.css +63 -97
  40. iatoolkit/static/styles/chat_public.css +107 -0
  41. iatoolkit/static/styles/landing_page.css +0 -1
  42. iatoolkit/templates/_company_header.html +6 -2
  43. iatoolkit/templates/_login_widget.html +42 -0
  44. iatoolkit/templates/base.html +34 -19
  45. iatoolkit/templates/change_password.html +22 -20
  46. iatoolkit/templates/chat.html +58 -27
  47. iatoolkit/templates/chat_modals.html +113 -74
  48. iatoolkit/templates/error.html +12 -13
  49. iatoolkit/templates/forgot_password.html +11 -7
  50. iatoolkit/templates/index.html +8 -3
  51. iatoolkit/templates/login_simulation.html +16 -5
  52. iatoolkit/templates/onboarding_shell.html +0 -1
  53. iatoolkit/templates/signup.html +14 -14
  54. iatoolkit/views/base_login_view.py +12 -1
  55. iatoolkit/views/change_password_view.py +49 -33
  56. iatoolkit/views/forgot_password_view.py +20 -19
  57. iatoolkit/views/help_content_api_view.py +54 -0
  58. iatoolkit/views/history_api_view.py +13 -9
  59. iatoolkit/views/home_view.py +30 -38
  60. iatoolkit/views/init_context_api_view.py +16 -11
  61. iatoolkit/views/llmquery_api_view.py +38 -26
  62. iatoolkit/views/login_simulation_view.py +14 -2
  63. iatoolkit/views/login_view.py +47 -35
  64. iatoolkit/views/logout_api_view.py +26 -22
  65. iatoolkit/views/profile_api_view.py +46 -0
  66. iatoolkit/views/prompt_api_view.py +6 -6
  67. iatoolkit/views/signup_view.py +26 -24
  68. iatoolkit/views/user_feedback_api_view.py +19 -18
  69. iatoolkit/views/verify_user_view.py +30 -29
  70. {iatoolkit-0.63.1.dist-info → iatoolkit-0.67.0.dist-info}/METADATA +40 -22
  71. iatoolkit-0.67.0.dist-info/RECORD +120 -0
  72. iatoolkit-0.67.0.dist-info/licenses/LICENSE +21 -0
  73. iatoolkit/static/styles/chat_info.css +0 -53
  74. iatoolkit/templates/header.html +0 -31
  75. iatoolkit/templates/test.html +0 -9
  76. iatoolkit-0.63.1.dist-info/RECORD +0 -112
  77. {iatoolkit-0.63.1.dist-info → iatoolkit-0.67.0.dist-info}/WHEEL +0 -0
  78. {iatoolkit-0.63.1.dist-info → iatoolkit-0.67.0.dist-info}/top_level.txt +0 -0
@@ -6,32 +6,33 @@
6
6
  from injector import inject
7
7
  from iatoolkit.repositories.llm_query_repo import LLMQueryRepo
8
8
  from iatoolkit.repositories.profile_repo import ProfileRepo
9
+ from iatoolkit.services.i18n_service import I18nService
10
+
9
11
 
10
12
 
11
13
  class HistoryService:
12
14
  @inject
13
15
  def __init__(self, llm_query_repo: LLMQueryRepo,
14
- profile_repo: ProfileRepo):
16
+ profile_repo: ProfileRepo,
17
+ i18n_service: I18nService):
15
18
  self.llm_query_repo = llm_query_repo
16
19
  self.profile_repo = profile_repo
20
+ self.i18n_service = i18n_service
17
21
 
18
22
  def get_history(self,
19
23
  company_short_name: str,
20
24
  user_identifier: str) -> dict:
21
25
  try:
22
- # validate company
23
26
  company = self.profile_repo.get_company_by_short_name(company_short_name)
24
27
  if not company:
25
- return {'error': f'No existe la empresa: {company_short_name}'}
28
+ return {"error": self.i18n_service.t('errors.company_not_found', company_short_name=company_short_name)}
26
29
 
27
30
  history = self.llm_query_repo.get_history(company, user_identifier)
28
-
29
31
  if not history:
30
- return {'message': 'Historial vacio actualmente', 'history': []}
32
+ return {'message': 'empty history', 'history': []}
31
33
 
32
34
  history_list = [query.to_dict() for query in history]
33
-
34
- return {'message': 'Historial obtenido correctamente', 'history': history_list}
35
+ return {'message': 'history loaded ok', 'history': history_list}
35
36
 
36
37
  except Exception as e:
37
38
  return {'error': str(e)}
@@ -0,0 +1,104 @@
1
+ # iatoolkit/services/i18n_service.py
2
+ import os
3
+ import logging
4
+ from injector import inject, singleton
5
+ from iatoolkit.common.util import Utility
6
+ from iatoolkit.services.language_service import LanguageService
7
+
8
+ @singleton
9
+ class I18nService:
10
+ """
11
+ Servicio centralizado para manejar la internacionalización (i18n).
12
+ Carga todas las traducciones desde archivos YAML en memoria al iniciar.
13
+ """
14
+ FALLBACK_LANGUAGE = 'es'
15
+
16
+ @inject
17
+ def __init__(self, util: Utility, language_service: LanguageService):
18
+ self.util = util
19
+ self.language_service = language_service
20
+
21
+ self.translations = {}
22
+ self._load_translations()
23
+
24
+ def _load_translations(self):
25
+ """
26
+ Carga todos los archivos .yaml del directorio 'locales' en memoria.
27
+ """
28
+ locales_dir = os.path.join(os.path.dirname(__file__), '..', 'locales')
29
+ if not os.path.exists(locales_dir):
30
+ logging.error("Directory 'locales' not found.")
31
+ return
32
+
33
+ for filename in os.listdir(locales_dir):
34
+ if filename.endswith('.yaml'):
35
+ lang_code = filename.split('.')[0]
36
+ filepath = os.path.join(locales_dir, filename)
37
+ try:
38
+ self.translations[lang_code] = self.util.load_schema_from_yaml(filepath)
39
+ except Exception as e:
40
+ logging.error(f"Error while loading the translation file {filepath}: {e}")
41
+
42
+ def _get_nested_key(self, lang: str, key: str):
43
+ """
44
+ Obtiene un valor de un diccionario anidado usando una clave con puntos.
45
+ """
46
+ data = self.translations.get(lang, {})
47
+ keys = key.split('.')
48
+ for k in keys:
49
+ if isinstance(data, dict) and k in data:
50
+ data = data[k]
51
+ else:
52
+ return None
53
+ return data
54
+
55
+ def get_translation_block(self, key: str, lang: str = None) -> dict:
56
+ """
57
+ Gets a whole dictionary block from the translations.
58
+ Useful for passing a set of translations to JavaScript.
59
+ """
60
+ if lang is None:
61
+ lang = self.language_service.get_current_language()
62
+
63
+ # 1. Try to get the block in the requested language
64
+ block = self._get_nested_key(lang, key)
65
+
66
+ # 2. If not found, try the fallback language
67
+ if not isinstance(block, dict):
68
+ block = self._get_nested_key(self.FALLBACK_LANGUAGE, key)
69
+
70
+ return block if isinstance(block, dict) else {}
71
+
72
+ def t(self, key: str, lang: str = None, **kwargs) -> str:
73
+ """
74
+ Gets the translation for a given key.
75
+ If 'lang' is provided, it's used. Otherwise, it's determined automatically.
76
+ """
77
+ # If no specific language is requested, determine it from the current context.
78
+ if lang is None:
79
+ lang = self.language_service.get_current_language()
80
+
81
+ # 1. Attempt to get the translation in the requested language
82
+ message = self._get_nested_key(lang, key)
83
+
84
+ # 2. If not found, try the fallback language
85
+ if message is None and lang != self.FALLBACK_LANGUAGE:
86
+ logging.warning(
87
+ f"Translation key '{key}' not found for language '{lang}'. Attempting fallback to '{self.FALLBACK_LANGUAGE}'.")
88
+ message = self._get_nested_key(self.FALLBACK_LANGUAGE, key)
89
+
90
+ # 3. If still not found, return the key itself as a last resort
91
+ if message is None:
92
+ logging.error(
93
+ f"Translation key '{key}' not found, even in fallback '{self.FALLBACK_LANGUAGE}'.")
94
+ return key
95
+
96
+ # 4. If variables are provided, format the message
97
+ if kwargs:
98
+ try:
99
+ return message.format(**kwargs)
100
+ except KeyError as e:
101
+ logging.error(f"Error formatting key '{key}': missing variable {e} in arguments.")
102
+ return message
103
+
104
+ return message
@@ -20,8 +20,8 @@ class JWTService:
20
20
  self.secret_key = app.config['JWT_SECRET_KEY']
21
21
  self.algorithm = app.config['JWT_ALGORITHM']
22
22
  except KeyError as e:
23
- logging.error(f"Configuración JWT faltante en app.config: {e}. JWTService no funcionará correctamente.")
24
- raise RuntimeError(f"Configuración JWT esencial faltante: {e}")
23
+ logging.error(f"missing JWT configuration: {e}.")
24
+ raise RuntimeError(f"missing JWT configuration variables: {e}")
25
25
 
26
26
  def generate_chat_jwt(self,
27
27
  company_short_name: str,
@@ -58,25 +58,23 @@ class JWTService:
58
58
 
59
59
  # Validaciones adicionales
60
60
  if payload.get('type') != 'chat_session':
61
- logging.warning(f"Validación JWT fallida: tipo incorrecto '{payload.get('type')}'")
61
+ logging.warning(f"Invalid JWT type '{payload.get('type')}'")
62
62
  return None
63
63
 
64
64
  # user_identifier debe estar presente
65
65
  if not payload.get('user_identifier'):
66
- logging.warning(f"Validación JWT fallida: user_identifier ausente o vacío.")
66
+ logging.warning(f"missing user_identifier in JWT payload.")
67
67
  return None
68
68
 
69
69
  if not payload.get('company_short_name'):
70
- logging.warning(f"Validación JWT fallida: company_short_name ausente.")
70
+ logging.warning(f"missing company_short_name in JWT payload.")
71
71
  return None
72
72
 
73
- logging.debug(
74
- f"JWT validado exitosamente para company: {payload.get('company_short_name')}, user: {payload.get('external_user_id')}")
75
73
  return payload
76
74
 
77
75
  except jwt.InvalidTokenError as e:
78
- logging.warning(f"Validación JWT fallida: token inválido . Error: {e}")
76
+ logging.warning(f"Invalid JWT token:: {e}")
79
77
  return None
80
78
  except Exception as e:
81
- logging.error(f"Error inesperado durante validación de JWT : {e}")
79
+ logging.error(f"unexpected error during JWT validation: {e}")
82
80
  return None
@@ -0,0 +1,79 @@
1
+ # iatoolkit/services/language_service.py
2
+
3
+ import logging
4
+ from injector import inject, singleton
5
+ from flask import g, request
6
+ from iatoolkit.repositories.profile_repo import ProfileRepo
7
+ from iatoolkit.common.session_manager import SessionManager
8
+
9
+ @singleton
10
+ class LanguageService:
11
+ """
12
+ Determines the correct language for the current request
13
+ based on a defined priority order (session, URL, etc.)
14
+ and caches it in the Flask 'g' object for the request's lifecycle.
15
+ """
16
+
17
+ FALLBACK_LANGUAGE = 'es'
18
+
19
+ @inject
20
+ def __init__(self, profile_repo: ProfileRepo):
21
+ self.profile_repo = profile_repo
22
+
23
+ def _get_company_short_name(self) -> str | None:
24
+ """
25
+ Gets the company_short_name from the current request context.
26
+ This handles different scenarios like web sessions, public URLs, and API calls.
27
+
28
+ Priority Order:
29
+ 1. Flask Session (for logged-in web users).
30
+ 2. URL rule variable (for public pages and API endpoints).
31
+ """
32
+ # 1. Check session for logged-in users
33
+ company_short_name = SessionManager.get('company_short_name')
34
+ if company_short_name:
35
+ return company_short_name
36
+
37
+ # 2. Check URL arguments (e.g., /<company_short_name>/login)
38
+ # This covers public pages and most API calls.
39
+ if request.view_args and 'company_short_name' in request.view_args:
40
+ return request.view_args['company_short_name']
41
+
42
+ return None
43
+
44
+ def get_current_language(self) -> str:
45
+ """
46
+ Determines and caches the language for the current request using a priority order:
47
+ 1. User's preference (from their profile).
48
+ 2. Company's default language.
49
+ 3. System-wide fallback language ('es').
50
+ """
51
+ if 'lang' in g:
52
+ return g.lang
53
+
54
+ try:
55
+ # Priority 1: User's preferred language
56
+ user_identifier = SessionManager.get('user_identifier')
57
+ if user_identifier:
58
+ user = self.profile_repo.get_user_by_email(user_identifier)
59
+ if user and user.preferred_language:
60
+ logging.debug(f"Language determined by user preference: {user.preferred_language}")
61
+ g.lang = user.preferred_language
62
+ return g.lang
63
+
64
+ # Priority 2: Company's default language
65
+ company_short_name = self._get_company_short_name()
66
+ if company_short_name:
67
+ company = self.profile_repo.get_company_by_short_name(company_short_name)
68
+ if company and company.default_language:
69
+ logging.debug(f"Language determined by company default: {company.default_language}")
70
+ g.lang = company.default_language
71
+ return g.lang
72
+ except Exception as e:
73
+ logging.info(f"Could not determine language, falling back to default. Reason: {e}")
74
+ pass
75
+
76
+ # Priority 3: System-wide fallback
77
+ logging.info(f"Language determined by system fallback: {self.FALLBACK_LANGUAGE}")
78
+ g.lang = self.FALLBACK_LANGUAGE
79
+ return g.lang
@@ -72,7 +72,7 @@ class LoadDocumentsService:
72
72
  """
73
73
  if not connector_config:
74
74
  raise IAToolkitException(IAToolkitException.ErrorType.MISSING_PARAMETER,
75
- f"Falta configurar conector")
75
+ f"Missing connector config")
76
76
 
77
77
  try:
78
78
  if not filters:
@@ -123,7 +123,7 @@ class LoadDocumentsService:
123
123
 
124
124
  if not company:
125
125
  raise IAToolkitException(IAToolkitException.ErrorType.MISSING_PARAMETER,
126
- f"Falta configurar empresa")
126
+ f"missing company")
127
127
 
128
128
  # check if file exist in repositories
129
129
  if self.doc_repo.get(company_id=company.id,filename=filename):
@@ -182,6 +182,6 @@ class LoadDocumentsService:
182
182
  self.doc_repo.session.rollback()
183
183
 
184
184
  # if something fails, throw exception
185
- logging.exception("Error procesando el archivo %s: %s", filename, str(e))
185
+ logging.exception("Error processing file %s: %s", filename, str(e))
186
186
  raise IAToolkitException(IAToolkitException.ErrorType.LOAD_DOCUMENT_ERROR,
187
- f"Error al procesar el archivo {filename}")
187
+ f"Error while processing file: {filename}")
@@ -4,6 +4,7 @@
4
4
  # IAToolkit is open source software.
5
5
 
6
6
  from iatoolkit.infra.mail_app import MailApp
7
+ from iatoolkit.services.i18n_service import I18nService
7
8
  from injector import inject
8
9
  from pathlib import Path
9
10
  from iatoolkit.common.exceptions import IAToolkitException
@@ -13,18 +14,22 @@ TEMP_DIR = Path("static/temp")
13
14
 
14
15
  class MailService:
15
16
  @inject
16
- def __init__(self, mail_app: MailApp):
17
+ def __init__(self,
18
+ mail_app: MailApp,
19
+ i18n_service: I18nService):
17
20
  self.mail_app = mail_app
21
+ self.i18n_service = i18n_service
22
+
18
23
 
19
24
  def _read_token_bytes(self, token: str) -> bytes:
20
25
  # Defensa simple contra path traversal
21
26
  if not token or "/" in token or "\\" in token or token.startswith("."):
22
27
  raise IAToolkitException(IAToolkitException.ErrorType.MAIL_ERROR,
23
- "attachment_token inválido")
28
+ "attachment_token invalid")
24
29
  path = TEMP_DIR / token
25
30
  if not path.is_file():
26
31
  raise IAToolkitException(IAToolkitException.ErrorType.MAIL_ERROR,
27
- f"Adjunto no encontrado: {token}")
32
+ f"attach file not found: {token}")
28
33
  return path.read_bytes()
29
34
 
30
35
  def send_mail(self, **kwargs):
@@ -59,4 +64,4 @@ class MailService:
59
64
  body=body,
60
65
  attachments=norm_attachments)
61
66
 
62
- return 'mail enviado'
67
+ return self.i18n_service.t('services.mail_sent')
@@ -4,7 +4,9 @@
4
4
  # IAToolkit is open source software.
5
5
 
6
6
  from iatoolkit.repositories.models import Company
7
+ from iatoolkit.services.configuration_service import ConfigurationService
7
8
  from typing import List, Dict, Any
9
+ from injector import inject
8
10
 
9
11
 
10
12
  class OnboardingService:
@@ -12,11 +14,13 @@ class OnboardingService:
12
14
  Servicio para gestionar las tarjetas de contenido que se muestran
13
15
  durante la pantalla de carga (onboarding).
14
16
  """
15
-
16
- def __init__(self):
17
+ @inject
18
+ def __init__(self, config_service: ConfigurationService):
17
19
  """
18
20
  Define el conjunto de tarjetas de onboarding por defecto.
19
21
  """
22
+ self.config_service = config_service
23
+
20
24
  self._default_cards = [
21
25
  {'icon': 'fas fa-users', 'title': 'Clientes',
22
26
  'text': 'Conozco en detalle a nuestros clientes: antigüedad, contactos, historial de operaciones.<br><br><strong>Ejemplo:</strong> ¿cuántos clientes nuevos se incorporaron a mi cartera este año?'},
@@ -37,7 +41,9 @@ class OnboardingService:
37
41
  Si la compañía tiene tarjetas personalizadas, las devuelve.
38
42
  De lo contrario, devuelve las tarjetas por defecto.
39
43
  """
40
- if company and company.onboarding_cards:
41
- return company.onboarding_cards
44
+ if company:
45
+ onboarding_cards = self.config_service.get_company_content(company.short_name, 'onboarding_cards')
46
+
47
+ return onboarding_cards
42
48
 
43
49
  return self._default_cards
@@ -5,7 +5,7 @@
5
5
 
6
6
  from injector import inject
7
7
  from iatoolkit.repositories.profile_repo import ProfileRepo
8
- from iatoolkit.services.dispatcher_service import Dispatcher
8
+ from iatoolkit.services.i18n_service import I18nService
9
9
  from iatoolkit.repositories.models import User, Company, ApiKey
10
10
  from flask_bcrypt import check_password_hash
11
11
  from iatoolkit.common.session_manager import SessionManager
@@ -16,16 +16,19 @@ import random
16
16
  import re
17
17
  import secrets
18
18
  import string
19
+ import logging
19
20
  from iatoolkit.services.dispatcher_service import Dispatcher
20
21
 
21
22
 
22
23
  class ProfileService:
23
24
  @inject
24
25
  def __init__(self,
26
+ i18n_service: I18nService,
25
27
  profile_repo: ProfileRepo,
26
28
  session_context_service: UserSessionContextService,
27
29
  dispatcher: Dispatcher,
28
30
  mail_app: MailApp):
31
+ self.i18n_service = i18n_service
29
32
  self.profile_repo = profile_repo
30
33
  self.dispatcher = dispatcher
31
34
  self.session_context = session_context_service
@@ -38,23 +41,23 @@ class ProfileService:
38
41
  # check if user exists
39
42
  user = self.profile_repo.get_user_by_email(email)
40
43
  if not user:
41
- return {'success': False, "message": "Usuario no encontrado"}
44
+ return {'success': False, 'message': self.i18n_service.t('errors.auth.user_not_found')}
42
45
 
43
46
  # check the encrypted password
44
47
  if not check_password_hash(user.password, password):
45
- return {'success': False, "message": "Contraseña inválida"}
48
+ return {'success': False, 'message': self.i18n_service.t('errors.auth.invalid_password')}
46
49
 
47
50
  company = self.profile_repo.get_company_by_short_name(company_short_name)
48
51
  if not company:
49
- return {'success': False, "message": "Empresa no encontrada"}
52
+ return {'success': False, "message": "missing company"}
50
53
 
51
54
  # check that user belongs to company
52
55
  if company not in user.companies:
53
- return {'success': False, "message": "Usuario no esta autorizado para esta empresa"}
56
+ return {'success': False, "message": self.i18n_service.t('errors.services.user_not_authorized')}
54
57
 
55
58
  if not user.verified:
56
59
  return {'success': False,
57
- "message": "Tu cuenta no ha sido verificada. Por favor, revisa tu correo."}
60
+ "message": self.i18n_service.t('errors.services.account_not_verified')}
58
61
 
59
62
  # 1. Build the local user profile dictionary here.
60
63
  # the user_profile variables are used on the LLM templates also (see in query_main.prompt)
@@ -71,8 +74,9 @@ class ProfileService:
71
74
 
72
75
  # 3. create the web session
73
76
  self.set_session_for_user(company.short_name, user_identifier)
74
- return {'success': True, "user_identifier": user_identifier, "message": "Login exitoso"}
77
+ return {'success': True, "user_identifier": user_identifier, "message": "Login ok"}
75
78
  except Exception as e:
79
+ logging.error(f"Error in login: {e}")
76
80
  return {'success': False, "message": str(e)}
77
81
 
78
82
  def create_external_user_profile_context(self, company: Company, user_identifier: str):
@@ -131,6 +135,24 @@ class ProfileService:
131
135
  "profile": profile
132
136
  }
133
137
 
138
+ def update_user_language(self, user_identifier: str, new_lang: str) -> dict:
139
+ """
140
+ Business logic to update a user's preferred language.
141
+ It validates the language and then calls the generic update method.
142
+ """
143
+ # 1. Validate that the language is supported by checking the loaded translations.
144
+ if new_lang not in self.i18n_service.translations:
145
+ return {'success': False, 'error_message': self.i18n_service.t('errors.general.unsupported_language')}
146
+
147
+ try:
148
+ # 2. Call the generic update_user method, passing the specific field to update.
149
+ self.update_user(user_identifier, preferred_language=new_lang)
150
+ return {'success': True, 'message': 'Language updated successfully.'}
151
+ except Exception as e:
152
+ # Log the error and return a generic failure message.
153
+ logging.error(f"Failed to update language for {user_identifier}: {e}")
154
+ return {'success': False, 'error_message': self.i18n_service.t('errors.general.unexpected_error', error=str(e))}
155
+
134
156
 
135
157
  def get_profile_by_identifier(self, company_short_name: str, user_identifier: str) -> dict:
136
158
  """
@@ -155,7 +177,8 @@ class ProfileService:
155
177
  # get company info
156
178
  company = self.profile_repo.get_company_by_short_name(company_short_name)
157
179
  if not company:
158
- return {"error": f"la empresa {company_short_name} no existe"}
180
+ return {
181
+ "error": self.i18n_service.t('errors.signup.company_not_found', company_name=company_short_name)}
159
182
 
160
183
  # normalize format's
161
184
  email = email.lower()
@@ -165,24 +188,25 @@ class ProfileService:
165
188
  if existing_user:
166
189
  # validate password
167
190
  if not self.bcrypt.check_password_hash(existing_user.password, password):
168
- return {"error": "La contraseña es incorrecta. No se puede agregar a la nueva empresa."}
191
+ return {"error": self.i18n_service.t('errors.signup.incorrect_password_for_existing_user', email=email)}
169
192
 
170
193
  # check if register
171
194
  if company in existing_user.companies:
172
- return {"error": "Usuario ya registrado en esta empresa"}
195
+ return {"error": self.i18n_service.t('errors.signup.user_already_registered', email=email)}
173
196
  else:
174
197
  # add new company to existing user
175
198
  existing_user.companies.append(company)
176
199
  self.profile_repo.save_user(existing_user)
177
- return {"message": "Usuario asociado a nueva empresa"}
200
+ return {"message": self.i18n_service.t('flash_messages.user_associated_success')}
178
201
 
179
202
  # add the new user
180
203
  if password != confirm_password:
181
- return {"error": "Las contraseñas no coinciden. Por favor, inténtalo de nuevo."}
204
+ return {"error": self.i18n_service.t('errors.signup.password_mismatch')}
182
205
 
183
206
  is_valid, message = self.validate_password(password)
184
207
  if not is_valid:
185
- return {"error": message}
208
+ # Translate the key returned by validate_password
209
+ return {"error": self.i18n_service.t(message)}
186
210
 
187
211
  # encrypt the password
188
212
  hashed_password = self.bcrypt.generate_password_hash(password).decode('utf-8')
@@ -204,9 +228,9 @@ class ProfileService:
204
228
  # send email with verification
205
229
  self.send_verification_email(new_user, company_short_name)
206
230
 
207
- return {"message": "Registro exitoso. Por favor, revisa tu correo para verificar tu cuenta."}
231
+ return {"message": self.i18n_service.t('flash_messages.signup_success')}
208
232
  except Exception as e:
209
- return {"error": str(e)}
233
+ return {"error": self.i18n_service.t('errors.general.unexpected_error', error=str(e))}
210
234
 
211
235
  def update_user(self, email: str, **kwargs) -> User:
212
236
  return self.profile_repo.update_user(email, **kwargs)
@@ -216,14 +240,14 @@ class ProfileService:
216
240
  # check if user exist
217
241
  user = self.profile_repo.get_user_by_email(email)
218
242
  if not user:
219
- return {"error": "El usuario no existe."}
243
+ return {"error": self.i18n_service.t('errors.verification.user_not_found')}
220
244
 
221
245
  # activate the user account
222
246
  self.profile_repo.verify_user(email)
223
- return {"message": "Tu cuenta ha sido verificada exitosamente. Ahora puedes iniciar sesión."}
247
+ return {"message": self.i18n_service.t('flash_messages.account_verified_success')}
224
248
 
225
249
  except Exception as e:
226
- return {"error": str(e)}
250
+ return {"error": self.i18n_service.t('errors.general.unexpected_error')}
227
251
 
228
252
  def change_password(self,
229
253
  email: str,
@@ -232,28 +256,28 @@ class ProfileService:
232
256
  confirm_password: str):
233
257
  try:
234
258
  if new_password != confirm_password:
235
- return {"error": "Las contraseñas no coinciden. Por favor, inténtalo nuevamente."}
259
+ return {"error": self.i18n_service.t('errors.change_password.password_mismatch')}
236
260
 
237
261
  # check the temporary code
238
262
  user = self.profile_repo.get_user_by_email(email)
239
263
  if not user or user.temp_code != temp_code:
240
- return {"error": "El código temporal no es válido. Por favor, verifica o solicita uno nuevo."}
264
+ return {"error": self.i18n_service.t('errors.change_password.invalid_temp_code')}
241
265
 
242
266
  # encrypt and save the password, make the temporary code invalid
243
267
  hashed_password = self.bcrypt.generate_password_hash(new_password).decode('utf-8')
244
268
  self.profile_repo.update_password(email, hashed_password)
245
269
  self.profile_repo.reset_temp_code(email)
246
270
 
247
- return {"message": "La clave se cambio correctamente"}
271
+ return {"message": self.i18n_service.t('flash_messages.password_changed_success')}
248
272
  except Exception as e:
249
- return {"error": str(e)}
273
+ return {"error": self.i18n_service.t('errors.general.unexpected_error')}
250
274
 
251
275
  def forgot_password(self, email: str, reset_url: str):
252
276
  try:
253
277
  # Verificar si el usuario existe
254
278
  user = self.profile_repo.get_user_by_email(email)
255
279
  if not user:
256
- return {"error": "El usuario no existe."}
280
+ return {"error": self.i18n_service.t('errors.forgot_password.user_not_registered', email=email)}
257
281
 
258
282
  # Gen a temporary code and store in the repositories
259
283
  temp_code = ''.join(random.choices(string.ascii_letters + string.digits, k=6)).upper()
@@ -262,35 +286,31 @@ class ProfileService:
262
286
  # send email to the user
263
287
  self.send_forgot_password_email(user, reset_url)
264
288
 
265
- return {"message": "se envio mail para cambio de clave"}
289
+ return {"message": self.i18n_service.t('flash_messages.forgot_password_success')}
266
290
  except Exception as e:
267
- return {"error": str(e)}
291
+ return {"error": self.i18n_service.t('errors.general.unexpected_error')}
268
292
 
269
293
  def validate_password(self, password):
270
294
  """
271
- Valida que una contraseña cumpla con los siguientes requisitos:
272
- - Al menos 8 caracteres de longitud
273
- - Contiene al menos una letra mayúscula
274
- - Contiene al menos una letra minúscula
275
- - Contiene al menos un número
276
- - Contiene al menos un carácter especial
295
+ Validates that a password meets all requirements.
296
+ Returns (True, "...") on success, or (False, "translation.key") on failure.
277
297
  """
278
298
  if len(password) < 8:
279
- return False, "La contraseña debe tener al menos 8 caracteres."
299
+ return False, "errors.validation.password_too_short"
280
300
 
281
301
  if not any(char.isupper() for char in password):
282
- return False, "La contraseña debe tener al menos una letra mayúscula."
302
+ return False, "errors.validation.password_no_uppercase"
283
303
 
284
304
  if not any(char.islower() for char in password):
285
- return False, "La contraseña debe tener al menos una letra minúscula."
305
+ return False, "errors.validation.password_no_lowercase"
286
306
 
287
307
  if not any(char.isdigit() for char in password):
288
- return False, "La contraseña debe tener al menos un número."
308
+ return False, "errors.validation.password_no_digit"
289
309
 
290
310
  if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
291
- return False, "La contraseña debe tener al menos un carácter especial."
311
+ return False, "errors.validation.password_no_special_char"
292
312
 
293
- return True, "La contraseña es válida."
313
+ return True, "Password is valid."
294
314
 
295
315
  def get_companies(self):
296
316
  return self.profile_repo.get_companies()
@@ -304,7 +324,7 @@ class ProfileService:
304
324
  def new_api_key(self, company_short_name: str):
305
325
  company = self.get_company_by_short_name(company_short_name)
306
326
  if not company:
307
- return {"error": f"la empresa {company_short_name} no existe"}
327
+ return {"error": self.i18n_service.t('errors.company_not_found', company_short_name=company_short_name)}
308
328
 
309
329
  length = 40 # lenght of the api key
310
330
  alphabet = string.ascii_letters + string.digits