Add traslations

This commit is contained in:
Urtzi Alfaro
2025-12-18 20:12:32 +01:00
parent f10a2b92ea
commit acb3a40844
15 changed files with 726 additions and 228 deletions

View File

@@ -86,8 +86,7 @@ export class TenantService {
return apiClient.get<TenantResponse[]>(`${this.baseUrl}/${parentTenantId}/children`);
}
async bulkCreateChildTenants(request: {
parent_tenant_id: string;
async bulkCreateChildTenants(parentTenantId: string, request: {
child_tenants: Array<{
name: string;
city: string;
@@ -99,6 +98,10 @@ export class TenantService {
longitude?: number;
phone?: string;
email?: string;
business_type?: string;
business_model?: string;
timezone?: string;
metadata?: Record<string, any>;
}>;
auto_configure_distribution?: boolean;
}): Promise<{
@@ -109,7 +112,7 @@ export class TenantService {
failed_tenants: Array<{ name: string; location_code: string; error: string }>;
distribution_configured: boolean;
}> {
return apiClient.post(`${this.baseUrl}/bulk-children`, request);
return apiClient.post(`${this.baseUrl}/${parentTenantId}/bulk-children`, request);
}
// ===================================================================

View File

@@ -154,7 +154,7 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
}
if (!formData.acceptTerms) {
newErrors.acceptTerms = t('auth:validation.terms_required', 'Debes aceptar los términos y condiciones');
newErrors.acceptTerms = t('auth:validation.terms_required', 'Debes aceptar los términos y condiciones y la política de privacidad');
}
setErrors(newErrors);
@@ -235,7 +235,7 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
const handlePaymentError = (errorMessage: string) => {
showToast.error(errorMessage, {
title: 'Error en el pago'
title: t('auth:alerts.payment_error', 'Error en el pago')
});
};
@@ -252,13 +252,13 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
// Show 2 steps if plan is pre-selected, 3 steps otherwise
const steps = preSelectedPlan
? [
{ key: 'basic_info', label: 'Información', number: 1, time: '2 min' },
{ key: 'payment', label: 'Pago', number: 2, time: '2 min' }
{ key: 'basic_info', label: t('auth:steps.info', 'Información'), number: 1, time: '2 min' },
{ key: 'payment', label: t('auth:steps.payment', 'Pago'), number: 2, time: '2 min' }
]
: [
{ key: 'basic_info', label: 'Información', number: 1, time: '2 min' },
{ key: 'subscription', label: 'Plan', number: 2, time: '1 min' },
{ key: 'payment', label: 'Pago', number: 3, time: '2 min' }
{ key: 'basic_info', label: t('auth:steps.info', 'Información'), number: 1, time: '2 min' },
{ key: 'subscription', label: t('auth:steps.subscription', 'Plan'), number: 2, time: '1 min' },
{ key: 'payment', label: t('auth:steps.payment', 'Pago'), number: 3, time: '2 min' }
];
const getStepIndex = (step: RegistrationStep) => {
@@ -499,14 +499,8 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
disabled={isLoading}
/>
<label htmlFor="acceptTerms" className="text-sm text-text-secondary cursor-pointer">
Acepto los{' '}
<a href="/terms" target="_blank" rel="noopener noreferrer" className="text-color-primary hover:text-color-primary-dark underline">
términos y condiciones
</a>{' '}
y la{' '}
<a href="/privacy" target="_blank" rel="noopener noreferrer" className="text-color-primary hover:text-color-primary-dark underline">
política de privacidad
</a>{' '}
{t('auth:register.accept_terms_and_privacy', 'Acepto los términos y condiciones y la política de privacidad')}
{' '}
<span className="text-color-error">*</span>
</label>
</div>
@@ -524,7 +518,7 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
disabled={isLoading}
/>
<label htmlFor="marketingConsent" className="text-sm text-text-secondary cursor-pointer">
Deseo recibir comunicaciones de marketing y promociones
{t('auth:register.marketing_consent', 'Deseo recibir comunicaciones de marketing y promociones')}
</label>
</div>
@@ -538,7 +532,7 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
disabled={isLoading}
/>
<label htmlFor="analyticsConsent" className="text-sm text-text-secondary cursor-pointer">
Acepto el uso de cookies analíticas para mejorar la experiencia
{t('auth:register.analytics_consent', 'Acepto el uso de cookies analíticas para mejorar la experiencia')}
</label>
</div>
</div>
@@ -551,7 +545,7 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
disabled={isLoading || !validatePassword(formData.password) || !formData.acceptTerms || passwordMatchStatus !== 'match'}
className="w-full sm:w-48"
>
Siguiente
{t('auth:register.next_button', 'Siguiente')}
</Button>
</div>
</form>
@@ -587,7 +581,7 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
disabled={isLoading}
className="w-full sm:w-48 order-2 sm:order-1"
>
Anterior
{t('auth:register.previous_button', 'Anterior')}
</Button>
<Button
variant="primary"
@@ -596,7 +590,7 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
disabled={isLoading}
className="w-full sm:w-48 order-1 sm:order-2"
>
Siguiente
{t('auth:register.next_button', 'Siguiente')}
</Button>
</div>
</div>
@@ -619,36 +613,36 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
<Card className="p-6 bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 border-2 border-blue-200 dark:border-blue-800">
<h3 className="text-lg font-bold text-text-primary mb-4 flex items-center gap-2">
<CheckCircle className="w-5 h-5 text-color-primary" />
Resumen de tu Plan
{t('auth:payment.payment_summary', 'Resumen de tu Plan')}
</h3>
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-text-secondary">Plan seleccionado:</span>
<span className="text-text-secondary">{t('auth:payment.selected_plan', 'Plan seleccionado:')}</span>
<span className="font-bold text-color-primary text-lg">{selectedPlanMetadata.name}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-text-secondary">Precio mensual:</span>
<span className="text-text-secondary">{t('auth:payment.monthly_price', 'Precio mensual:')}</span>
<span className="font-semibold text-text-primary">
{subscriptionService.formatPrice(selectedPlanMetadata.monthly_price)}/mes
</span>
</div>
{useTrial && (
<div className="flex justify-between items-center pt-3 border-t border-blue-200 dark:border-blue-800">
<span className="text-green-700 dark:text-green-400 font-medium">Período de prueba:</span>
<span className="text-green-700 dark:text-green-400 font-medium">{t('auth:payment.trial_period', 'Período de prueba:')}</span>
<span className="font-bold text-green-700 dark:text-green-400">
{isPilot ? `${trialMonths} meses GRATIS` : '14 días gratis'}
{isPilot ? t('auth:payment.free_months', {count: trialMonths}) : t('auth:payment.free_days', '14 días gratis')}
</span>
</div>
)}
<div className="pt-3 border-t border-blue-200 dark:border-blue-800">
<div className="flex justify-between items-center text-sm">
<span className="text-text-tertiary">Total hoy:</span>
<span className="text-text-tertiary">{t('auth:payment.total_today', 'Total hoy:')}</span>
<span className="font-bold text-xl text-color-success">0.00</span>
</div>
<p className="text-xs text-text-tertiary mt-2 text-center">
{useTrial
? `Se te cobrará ${subscriptionService.formatPrice(selectedPlanMetadata.monthly_price)} después del período de prueba`
: 'Tarjeta requerida para validación'
? t('auth:payment.billing_message', {price: subscriptionService.formatPrice(selectedPlanMetadata.monthly_price)})
: t('auth:payment.payment_required', 'Tarjeta requerida para validación')
}
</p>
</div>
@@ -675,7 +669,7 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
disabled={isLoading}
className="w-full sm:w-48"
>
Anterior
{t('auth:register.previous_button', 'Anterior')}
</Button>
</div>
</div>
@@ -701,7 +695,7 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
{onLoginClick && currentStep === 'basic_info' && (
<div className="mt-8 text-center border-t border-border-primary pt-6">
<p className="text-text-secondary mb-4">
¿Ya tienes una cuenta?
{t('auth:register.have_account', '¿Ya tienes una cuenta?')}
</p>
<Button
variant="ghost"
@@ -709,7 +703,7 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
disabled={isLoading}
className="text-color-primary hover:text-color-primary-dark"
>
Iniciar Sesión
{t('auth:register.sign_in_link', 'Iniciar Sesión')}
</Button>
</div>
)}

View File

@@ -422,8 +422,7 @@ const OnboardingWizardContent: React.FC = () => {
throw new Error('Parent tenant not registered');
}
const response = await tenantService.bulkCreateChildTenants({
parent_tenant_id: parentTenantId,
const response = await tenantService.bulkCreateChildTenants(parentTenantId, {
child_tenants: data.childTenants.map((ct: any) => ({
name: ct.name,
city: ct.city,
@@ -435,6 +434,10 @@ const OnboardingWizardContent: React.FC = () => {
longitude: ct.longitude,
phone: ct.phone,
email: ct.email,
business_type: ct.business_type,
business_model: ct.business_model,
timezone: ct.timezone,
metadata: ct.metadata,
})),
auto_configure_distribution: true,
});
@@ -446,6 +449,11 @@ const OnboardingWizardContent: React.FC = () => {
if (response.failed_count > 0) {
console.warn('⚠️ Some child tenants failed to create:', response.failed_tenants);
// Show specific errors for each failed tenant
response.failed_tenants.forEach(failed => {
console.error(`Failed to create tenant ${failed.name} (${failed.location_code}):`, failed.error);
});
}
} catch (childTenantError) {
console.error('❌ Failed to create child tenants:', childTenantError);

View File

@@ -1,10 +1,12 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Plus, Store, MapPin, Trash2, Edit2, Building2, X } from 'lucide-react';
import { Plus, Store, MapPin, Trash2, Edit2, Building2, X, Phone, Mail, Clock, Tag, Globe } from 'lucide-react';
import Button from '../../../ui/Button/Button';
import Card from '../../../ui/Card/Card';
import { Modal, ModalHeader, ModalBody, ModalFooter } from '../../../ui/Modal';
import { Input } from '../../../ui/Input';
import { Select } from '../../../ui/Select';
import { AddressAutocomplete } from '../../../ui/AddressAutocomplete';
export interface ChildTenantSetupStepProps {
onUpdate?: (data: { childTenants: ChildTenant[]; canContinue: boolean }) => void;
@@ -22,6 +24,14 @@ export interface ChildTenant {
address: string;
postal_code: string;
location_code: string;
latitude?: number;
longitude?: number;
phone?: string;
email?: string;
business_type?: string;
business_model?: string;
timezone?: string;
metadata?: Record<string, any>;
}
export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
@@ -42,6 +52,14 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
address: '',
postal_code: '',
location_code: '',
latitude: undefined,
longitude: undefined,
phone: '',
email: '',
business_type: 'bakery',
business_model: 'retail_bakery',
timezone: 'Europe/Madrid',
metadata: {},
});
const [formErrors, setFormErrors] = useState<Record<string, string>>({});
@@ -67,11 +85,35 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
}
if (!formData.postal_code?.trim()) {
errors.postal_code = 'El código postal es requerido';
} else if (!/^\d{5}$/.test(formData.postal_code)) {
errors.postal_code = 'El código postal debe tener exactamente 5 dígitos';
}
if (!formData.location_code?.trim()) {
errors.location_code = 'El código de ubicación es requerido';
} else if (formData.location_code.length > 10) {
errors.location_code = 'El código no debe exceder 10 caracteres';
} else if (!/^[A-Z0-9\-_.]+$/.test(formData.location_code)) {
errors.location_code = 'Solo se permiten letras mayúsculas, números y guiones/guiones bajos';
}
// Phone validation
if (formData.phone && formData.phone.trim()) {
const phone = formData.phone.replace(/[\s\-\(\)]/g, '');
const patterns = [
/^(\+34|0034|34)?[6789]\d{8}$/, // Mobile
/^(\+34|0034|34)?9\d{8}$/ // Landline
];
if (!patterns.some(pattern => pattern.test(phone))) {
errors.phone = 'Introduce un número de teléfono español válido';
}
}
// Email validation
if (formData.email && formData.email.trim()) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(formData.email)) {
errors.email = 'Introduce un correo electrónico válido';
}
}
setFormErrors(errors);
@@ -122,6 +164,14 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
address: formData.address!,
postal_code: formData.postal_code!,
location_code: formData.location_code!.toUpperCase(),
latitude: formData.latitude,
longitude: formData.longitude,
phone: formData.phone || undefined,
email: formData.email || undefined,
business_type: formData.business_type || 'bakery',
business_model: formData.business_model || 'retail_bakery',
timezone: formData.timezone || 'Europe/Madrid',
metadata: formData.metadata || {},
};
if (editingTenant) {
@@ -236,9 +286,16 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
<h3 className="font-semibold text-[var(--text-primary)]">
{tenant.name}
</h3>
<span className="text-xs text-[var(--text-tertiary)] font-mono">
{tenant.location_code}
</span>
<div className="flex gap-2">
<span className="text-xs text-[var(--text-tertiary)] font-mono">
{tenant.location_code}
</span>
{tenant.zone && (
<span className="text-xs text-[var(--text-tertiary)]">
{tenant.zone}
</span>
)}
</div>
</div>
</div>
<div className="flex gap-1">
@@ -336,7 +393,142 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
</p>
</div>
{/* City and Zone */}
{/* Business Type */}
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Tipo de Negocio
</label>
<Select
value={formData.business_type || 'bakery'}
onChange={(e) => setFormData({ ...formData, business_type: e.target.value })}
>
<option value="bakery">Panadería</option>
<option value="coffee_shop">Cafetería</option>
<option value="pastry_shop">Pastelería</option>
<option value="restaurant">Restaurante</option>
</Select>
</div>
{/* Business Model */}
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Modelo de Negocio
</label>
<Select
value={formData.business_model || 'retail_bakery'}
onChange={(e) => setFormData({ ...formData, business_model: e.target.value })}
>
<option value="retail_bakery">Panadería Minorista</option>
<option value="central_baker_satellite">Obrador Central + Sucursales</option>
<option value="hybrid_bakery">Modelo Híbrido</option>
</Select>
</div>
{/* Contact Info */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Teléfono
</label>
<div className="relative">
<Phone className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-[var(--text-secondary)]" />
<Input
value={formData.phone || ''}
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
placeholder="ej. +34 123 456 789"
error={formErrors.phone}
className="pl-10"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Email
</label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-[var(--text-secondary)]" />
<Input
value={formData.email || ''}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
placeholder="ej. contacto@panaderia.com"
error={formErrors.email}
className="pl-10"
/>
</div>
</div>
</div>
{/* Timezone */}
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Zona Horaria
</label>
<div className="relative">
<Clock className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-[var(--text-secondary)]" />
<Select
value={formData.timezone || 'Europe/Madrid'}
onChange={(e) => setFormData({ ...formData, timezone: e.target.value })}
className="pl-10"
>
<option value="Europe/Madrid">Europe/Madrid (UTC+1/UTC+2)</option>
<option value="Europe/Paris">Europe/Paris (UTC+1/UTC+2)</option>
<option value="Europe/London">Europe/London (UTC+0/UTC+1)</option>
<option value="America/New_York">America/New_York (UTC-5/UTC-4)</option>
</Select>
</div>
</div>
{/* Zone */}
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Zona / Barrio (opcional)
</label>
<Input
value={formData.zone || ''}
onChange={(e) => setFormData({ ...formData, zone: e.target.value })}
placeholder="ej. Salamanca, Chamberí, Centro"
/>
<p className="text-xs text-[var(--text-tertiary)] mt-1">
Zona o barrio específico dentro de la ciudad
</p>
</div>
{/* Address with Autocomplete */}
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Dirección *
</label>
<AddressAutocomplete
value={formData.address || ''}
placeholder="ej. Calle de Serrano, 48"
onAddressSelect={(address) => {
setFormData(prev => ({
...prev,
address: address.display_name,
city: address.address.city || address.address.municipality || address.address.suburb || prev.city,
postal_code: address.address.postcode || prev.postal_code,
latitude: address.lat,
longitude: address.lon,
}));
}}
onCoordinatesChange={(lat, lon) => {
setFormData(prev => ({
...prev,
latitude: lat,
longitude: lon,
}));
}}
countryCode="es"
required
/>
{formErrors.address && (
<div className="mt-1 text-sm text-[var(--color-error)]">
{formErrors.address}
</div>
)}
</div>
{/* City and Postal Code */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
@@ -351,41 +543,17 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Zona / Barrio
Código Postal *
</label>
<Input
value={formData.zone || ''}
onChange={(e) => setFormData({ ...formData, zone: e.target.value })}
placeholder="ej. Salamanca"
value={formData.postal_code || ''}
onChange={(e) => setFormData({ ...formData, postal_code: e.target.value })}
placeholder="ej. 28001"
error={formErrors.postal_code}
maxLength={5}
/>
</div>
</div>
{/* Address */}
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Dirección *
</label>
<Input
value={formData.address || ''}
onChange={(e) => setFormData({ ...formData, address: e.target.value })}
placeholder="ej. Calle de Serrano, 48"
error={formErrors.address}
/>
</div>
{/* Postal Code */}
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Código Postal *
</label>
<Input
value={formData.postal_code || ''}
onChange={(e) => setFormData({ ...formData, postal_code: e.target.value })}
placeholder="ej. 28001"
error={formErrors.postal_code}
/>
</div>
</div>
</ModalBody>
<ModalFooter justify="end">

View File

@@ -35,7 +35,9 @@
"passwords_dont_match": "Passwords don't match",
"accept_terms": "I accept the terms and conditions",
"accept_privacy": "I accept the privacy policy",
"accept_terms_and_privacy": "I accept the terms and conditions and privacy policy",
"marketing_consent": "I want to receive newsletters and updates (optional)",
"analytics_consent": "I agree to the use of analytical cookies to improve the experience",
"register_button": "Create account",
"registering": "Creating account...",
"have_account": "Already have an account?",
@@ -44,7 +46,48 @@
"privacy_link": "Privacy Policy",
"step_of": "Step {current} of {total}",
"continue": "Continue",
"back": "Back"
"back": "Back",
"next_button": "Next",
"previous_button": "Previous",
"have_account": "Already have an account?",
"sign_in_link": "Sign in"
},
"steps": {
"info": "Information",
"subscription": "Plan",
"payment": "Payment"
},
"subscription": {
"select_plan": "Select your plan",
"choose_plan": "Choose the plan that best fits your business"
},
"payment": {
"payment_summary": "Your Plan Summary",
"selected_plan": "Selected plan:",
"monthly_price": "Monthly price:",
"trial_period": "Trial period:",
"total_today": "Total today:",
"payment_required": "Payment required for validation",
"billing_message": "You will be charged {price} after the trial period",
"free_months": "{count} months FREE",
"free_days": "14 days free",
"payment_info": "Payment information",
"secure_payment": "Your payment information is protected with end-to-end encryption",
"dev_mode": "Development Mode",
"cardholder_name": "Cardholder name",
"email": "Email address",
"address_line1": "Address",
"city": "City",
"state": "State/Province",
"postal_code": "Postal Code",
"country": "Country",
"card_details": "Card details",
"card_info_secure": "Your card information is secure"
},
"alerts": {
"success_create": "Account created successfully",
"error_create": "Error creating account",
"payment_error": "Payment error"
},
"forgot_password": {
"title": "Reset password",
@@ -124,14 +167,14 @@
"email_required": "Email address is required",
"email_invalid": "Please enter a valid email address",
"password_required": "Password is required",
"password_min_length": "Password must be at least {{min}} characters",
"password_min_length": "Password must be at least {min} characters",
"password_weak": "Password is too weak",
"passwords_must_match": "Passwords must match",
"first_name_required": "First name is required",
"last_name_required": "Last name is required",
"phone_required": "Phone is required",
"company_name_required": "Company name is required",
"terms_required": "You must accept the terms and conditions",
"terms_required": "You must accept the terms and conditions and privacy policy",
"field_required": "This field is required",
"invalid_format": "Invalid format"
},

View File

@@ -441,5 +441,31 @@
"features": "Features",
"about": "About",
"contact": "Contact"
},
"messages": {
"saved": "Saved successfully",
"created": "Created successfully",
"updated": "Updated successfully",
"deleted": "Deleted successfully",
"sent": "Sent successfully",
"imported": "Imported successfully",
"exported": "Exported successfully",
"logged_in": "Logged in successfully",
"logged_out": "Logged out successfully"
},
"errors": {
"required_field": "This field is required",
"invalid_email": "Invalid email address",
"invalid_phone": "Invalid phone number",
"weak_password": "Password must be stronger",
"passwords_not_match": "Passwords do not match",
"network_error": "Network error",
"server_error": "Server error",
"unauthorized": "Unauthorized",
"forbidden": "Access denied",
"not_found": "Not found",
"validation_error": "Validation error",
"file_too_large": "File too large",
"invalid_file_type": "Invalid file type"
}
}

View File

@@ -35,7 +35,9 @@
"passwords_dont_match": "Las contraseñas no coinciden",
"accept_terms": "Acepto los términos y condiciones",
"accept_privacy": "Acepto la política de privacidad",
"accept_terms_and_privacy": "Acepto los términos y condiciones y la política de privacidad",
"marketing_consent": "Quiero recibir newsletters y novedades (opcional)",
"analytics_consent": "Acepto el uso de cookies analíticas para mejorar la experiencia",
"register_button": "Crear cuenta",
"registering": "Creando cuenta...",
"have_account": "¿Ya tienes cuenta?",
@@ -44,7 +46,44 @@
"privacy_link": "Política de Privacidad",
"step_of": "Paso {current} de {total}",
"continue": "Continuar",
"back": "Atrás"
"back": "Atrás",
"next_button": "Siguiente",
"previous_button": "Anterior",
"have_account": "¿Ya tienes una cuenta?",
"sign_in_link": "Iniciar sesión"
},
"steps": {
"info": "Información",
"subscription": "Plan",
"payment": "Pago"
},
"payment": {
"payment_summary": "Resumen de tu Plan",
"selected_plan": "Plan seleccionado:",
"monthly_price": "Precio mensual:",
"trial_period": "Período de prueba:",
"total_today": "Total hoy:",
"payment_required": "Tarjeta requerida para validación",
"billing_message": "Se te cobrará {{price}} después del período de prueba",
"free_months": "{{count}} meses GRATIS",
"free_days": "14 días gratis",
"payment_info": "Información de Pago",
"secure_payment": "Tu información de pago está protegida con encriptación de extremo a extremo",
"dev_mode": "Modo Desarrollo",
"cardholder_name": "Nombre del titular",
"email": "Correo electrónico",
"address_line1": "Dirección",
"city": "Ciudad",
"state": "Estado/Provincia",
"postal_code": "Código Postal",
"country": "País",
"card_details": "Detalles de la tarjeta",
"card_info_secure": "Tu información de tarjeta está segura"
},
"alerts": {
"success_create": "Cuenta creada exitosamente",
"error_create": "Error al crear la cuenta",
"payment_error": "Error en el pago"
},
"forgot_password": {
"title": "Recuperar contraseña",
@@ -131,7 +170,7 @@
"last_name_required": "El apellido es requerido",
"phone_required": "El teléfono es requerido",
"company_name_required": "El nombre de la empresa es requerido",
"terms_required": "Debes aceptar los términos y condiciones",
"terms_required": "Debes aceptar los términos y condiciones y la política de privacidad",
"field_required": "Este campo es requerido",
"invalid_format": "Formato inválido"
},

View File

@@ -463,5 +463,31 @@
"features": "Funcionalidades",
"about": "Nosotros",
"contact": "Contacto"
},
"messages": {
"saved": "Guardado correctamente",
"created": "Creado correctamente",
"updated": "Actualizado correctamente",
"deleted": "Eliminado correctamente",
"sent": "Enviado correctamente",
"imported": "Importado correctamente",
"exported": "Exportado correctamente",
"logged_in": "Sesión iniciada correctamente",
"logged_out": "Sesión cerrada correctamente"
},
"errors": {
"required_field": "Este campo es obligatorio",
"invalid_email": "Email no válido",
"invalid_phone": "Teléfono no válido",
"weak_password": "La contraseña debe ser más segura",
"passwords_not_match": "Las contraseñas no coinciden",
"network_error": "Error de red",
"server_error": "Error del servidor",
"unauthorized": "No autorizado",
"forbidden": "Acceso denegado",
"not_found": "No encontrado",
"validation_error": "Error de validación",
"file_too_large": "Archivo demasiado grande",
"invalid_file_type": "Tipo de archivo no válido"
}
}

View File

@@ -35,7 +35,9 @@
"passwords_dont_match": "Pasahitzak ez datoz bat",
"accept_terms": "Baldintza eta baldintzak onartzen ditut",
"accept_privacy": "Pribatutasun politika onartzen dut",
"accept_terms_and_privacy": "Baldintzak eta pribatutasun politika onartzen ditut",
"marketing_consent": "Newsletter eta berriak jaso nahi ditut (aukerakoa)",
"analytics_consent": "Ados nago analitikarako cookieak erabiltzearekin esperientzia hobetzeko",
"register_button": "Sortu kontua",
"registering": "Kontua sortzen...",
"have_account": "Dagoeneko baduzu kontua?",
@@ -44,7 +46,75 @@
"privacy_link": "Pribatutasun politika",
"step_of": "{current}. urratsa {total}-tik",
"continue": "Jarraitu",
"back": "Atzera"
"back": "Atzera",
"next_button": "Hurrengoa",
"previous_button": "Aurrekoa"
},
"steps": {
"info": "Informazioa",
"subscription": "Harpidetza",
"payment": "Ordainketa"
},
"payment": {
"payment_summary": "Zure planaren laburpena",
"selected_plan": "Hautatutako plana:",
"monthly_price": "Hileroko prezioa:",
"trial_period": "Proba epea:",
"total_today": "Gaurko totala:",
"payment_required": "Ordainketa beharrezkoa balidaziorako",
"billing_message": "{{price}} kobratuko zaizu proba epea ondoren",
"free_months": "{{count}} hilabete DOAN",
"free_days": "14 egun doan",
"payment_info": "Ordainketaren informazioa",
"secure_payment": "Zure ordainketa informazioa babespetuta dago amaieratik amaierarako zifratzearekin",
"dev_mode": "Garapen Modua",
"cardholder_name": "Txartelaren titularra",
"email": "Helbide elektronikoa",
"address_line1": "Helbidea",
"city": "Hiria",
"state": "Estatua/Probintzia",
"postal_code": "Posta kodea",
"country": "Herrialdea",
"card_details": "Txartelaren xehetasunak",
"card_info_secure": "Zure txartelaren informazioa segurua da",
"process_payment": "Prozesatu Ordainketa",
"payment_bypassed_title": "Ordainketa Saltatua",
"payment_bypassed_description": "Ordainketa prozesua saltatu da garapen moduan. Erregistratzea modu normalean jarraituko du.",
"continue_registration": "Erregistratzearekin Jarraitu",
"payment_bypassed": "Ordainketa Saltatua",
"bypass_payment": "Saltatu Ordainketa"
},
"subscription": {
"select_plan": "Hautatu zure plana",
"choose_plan": "Aukeratu zure negozioari egokiena den plana"
},
"payment": {
"payment_summary": "Zure planaren laburpena",
"selected_plan": "Hautatutako plana:",
"monthly_price": "Hileroko prezioa:",
"trial_period": "Proba epea:",
"total_today": "Gaurko totala:",
"payment_required": "Ordainketa beharrezkoa balidaziorako",
"billing_message": "{price} kobratuko zaizu proba epea ondoren",
"free_months": "{count} hilabete DOAN",
"free_days": "14 egun doan",
"payment_info": "Ordainketa informazioa",
"secure_payment": "Zure ordainketa informazioa babespetuta dago amaieratik amaierarako zifratzearekin",
"dev_mode": "Garapen Modua",
"cardholder_name": "Txartelaren titularra",
"email": "Helbide elektronikoa",
"address_line1": "Helbidea",
"city": "Hiria",
"state": "Estatua/Probintzia",
"postal_code": "Posta kodea",
"country": "Herrialdea",
"card_details": "Txartelaren xehetasunak",
"card_info_secure": "Zure txartelaren informazioa segurua da"
},
"alerts": {
"success_create": "Kontua behar bezala sortu da",
"error_create": "Errorea kontua sortzean",
"payment_error": "Ordainketa errorea"
},
"forgot_password": {
"title": "Berrezarri pasahitza",
@@ -124,14 +194,14 @@
"email_required": "Helbide elektronikoa beharrezkoa da",
"email_invalid": "Mesedez, sartu baliozko helbide elektroniko bat",
"password_required": "Pasahitza beharrezkoa da",
"password_min_length": "Pasahitzak gutxienez {{min}} karaktere izan behar ditu",
"password_min_length": "Pasahitzak gutxienez {min} karaktere izan behar ditu",
"password_weak": "Pasahitza ahulegia da",
"passwords_must_match": "Pasahitzak bat etorri behar dira",
"first_name_required": "Izena beharrezkoa da",
"last_name_required": "Abizena beharrezkoa da",
"phone_required": "Telefonoa beharrezkoa da",
"company_name_required": "Enpresaren izena beharrezkoa da",
"terms_required": "Baldintza eta baldintzak onartu behar dituzu",
"terms_required": "Baldintzak eta pribatutasun politika onartu behar dituzu",
"field_required": "Eremu hau beharrezkoa da",
"invalid_format": "Formatu baliogabea"
},
@@ -162,5 +232,10 @@
"manage_sales": "Kudeatu salmentak",
"view_reports": "Ikusi txostenak",
"manage_settings": "Kudeatu ezarpenak"
},
"alerts": {
"success_create": "Kontua behar bezala sortu da",
"error_create": "Errorea kontua sortzean",
"payment_error": "Ordainketa errorea"
}
}

View File

@@ -437,5 +437,31 @@
"features": "Ezaugarriak",
"about": "Guri buruz",
"contact": "Harremanetan jarri"
},
"messages": {
"saved": "Ondo gordeta",
"created": "Ondo sortuta",
"updated": "Ondo eguneratuta",
"deleted": "Ondo ezabatuta",
"sent": "Ondo bidalia",
"imported": "Ondo inportatuta",
"exported": "Ondo esportatuta",
"logged_in": "Saioa ondo hasita",
"logged_out": "Saioa ondo itxita"
},
"errors": {
"required_field": "Eremu hau beharrezkoa da",
"invalid_email": "Email baliogabea",
"invalid_phone": "Telefono baliogabea",
"weak_password": "Pasahitza indartsuagoa izan behar da",
"passwords_not_match": "Pasahitzak ez datoz bat",
"network_error": "Sare-errorea",
"server_error": "Zerbitzari-errorea",
"unauthorized": "Baimenik gabe",
"forbidden": "Sarbidea ukatuta",
"not_found": "Ez da aurkitu",
"validation_error": "Balidazio-errorea",
"file_too_large": "Fitxategia handiegia da",
"invalid_file_type": "Fitxategi mota baliogabea"
}
}

View File

@@ -339,30 +339,30 @@ export const ERROR_CODES = {
// Success messages
export const SUCCESS_MESSAGES = {
SAVED: 'Guardado correctamente',
CREATED: 'Creado correctamente',
UPDATED: 'Actualizado correctamente',
DELETED: 'Eliminado correctamente',
SENT: 'Enviado correctamente',
IMPORTED: 'Importado correctamente',
EXPORTED: 'Exportado correctamente',
LOGGED_IN: 'Sesión iniciada',
LOGGED_OUT: 'Sesión cerrada',
SAVED: 'common:messages.saved',
CREATED: 'common:messages.created',
UPDATED: 'common:messages.updated',
DELETED: 'common:messages.deleted',
SENT: 'common:messages.sent',
IMPORTED: 'common:messages.imported',
EXPORTED: 'common:messages.exported',
LOGGED_IN: 'common:messages.logged_in',
LOGGED_OUT: 'common:messages.logged_out',
} as const;
// Error messages
export const ERROR_MESSAGES = {
REQUIRED_FIELD: 'Este campo es obligatorio',
INVALID_EMAIL: 'Email no válido',
INVALID_PHONE: 'Teléfono no válido',
WEAK_PASSWORD: 'La contraseña debe ser más segura',
PASSWORDS_NOT_MATCH: 'Las contraseñas no coinciden',
NETWORK_ERROR: 'Error de conexión',
SERVER_ERROR: 'Error del servidor',
UNAUTHORIZED: 'No autorizado',
FORBIDDEN: 'Acceso denegado',
NOT_FOUND: 'No encontrado',
VALIDATION_ERROR: 'Error de validación',
FILE_TOO_LARGE: 'Archivo demasiado grande',
INVALID_FILE_TYPE: 'Tipo de archivo no válido',
REQUIRED_FIELD: 'common:errors.required_field',
INVALID_EMAIL: 'common:errors.invalid_email',
INVALID_PHONE: 'common:errors.invalid_phone',
WEAK_PASSWORD: 'common:errors.weak_password',
PASSWORDS_NOT_MATCH: 'common:errors.passwords_not_match',
NETWORK_ERROR: 'common:errors.network_error',
SERVER_ERROR: 'common:errors.server_error',
UNAUTHORIZED: 'common:errors.unauthorized',
FORBIDDEN: 'common:errors.forbidden',
NOT_FOUND: 'common:errors.not_found',
VALIDATION_ERROR: 'common:errors.validation_error',
FILE_TOO_LARGE: 'common:errors.file_too_large',
INVALID_FILE_TYPE: 'common:errors.invalid_file_type',
} as const;

View File

@@ -0,0 +1,32 @@
import { useTranslation } from 'react-i18next';
import { SUCCESS_MESSAGES, ERROR_MESSAGES } from '../utils/constants';
/**
* Hook to get translated success messages
*/
export const useSuccessMessages = () => {
const { t } = useTranslation();
const getMessage = (key: keyof typeof SUCCESS_MESSAGES): string => {
const translationKey = SUCCESS_MESSAGES[key];
const [namespace, path] = translationKey.split(':');
return t(`${namespace}:${path}`);
};
return { getMessage };
};
/**
* Hook to get translated error messages
*/
export const useErrorMessages = () => {
const { t } = useTranslation();
const getMessage = (key: keyof typeof ERROR_MESSAGES): string => {
const translationKey = ERROR_MESSAGES[key];
const [namespace, path] = translationKey.split(':');
return t(`${namespace}:${path}`);
};
return { getMessage };
};