Add traslations 2

This commit is contained in:
Urtzi Alfaro
2025-12-25 18:59:56 +01:00
parent b95b86ee2c
commit 8a585058ed
6 changed files with 609 additions and 136 deletions

View File

@@ -75,25 +75,25 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
const errors: Record<string, string> = {}; const errors: Record<string, string> = {};
if (!formData.name?.trim()) { if (!formData.name?.trim()) {
errors.name = 'El nombre es requerido'; errors.name = t('onboarding:child_tenants.validation.name_required');
} }
if (!formData.city?.trim()) { if (!formData.city?.trim()) {
errors.city = 'La ciudad es requerida'; errors.city = t('onboarding:child_tenants.validation.city_required');
} }
if (!formData.address?.trim()) { if (!formData.address?.trim()) {
errors.address = 'La dirección es requerida'; errors.address = t('onboarding:child_tenants.validation.address_required');
} }
if (!formData.postal_code?.trim()) { if (!formData.postal_code?.trim()) {
errors.postal_code = 'El código postal es requerido'; errors.postal_code = t('onboarding:child_tenants.validation.postal_code_required');
} else if (!/^\d{5}$/.test(formData.postal_code)) { } else if (!/^\d{5}$/.test(formData.postal_code)) {
errors.postal_code = 'El código postal debe tener exactamente 5 dígitos'; errors.postal_code = t('onboarding:child_tenants.validation.postal_code_format');
} }
if (!formData.location_code?.trim()) { if (!formData.location_code?.trim()) {
errors.location_code = 'El código de ubicación es requerido'; errors.location_code = t('onboarding:child_tenants.validation.location_code_required');
} else if (formData.location_code.length > 10) { } else if (formData.location_code.length > 10) {
errors.location_code = 'El código no debe exceder 10 caracteres'; errors.location_code = t('onboarding:child_tenants.validation.location_code_max');
} else if (!/^[A-Z0-9\-_.]+$/.test(formData.location_code)) { } else if (!/^[A-Z0-9\-_.]+$/.test(formData.location_code)) {
errors.location_code = 'Solo se permiten letras mayúsculas, números y guiones/guiones bajos'; errors.location_code = t('onboarding:child_tenants.validation.location_code_format');
} }
// Phone validation // Phone validation
@@ -104,7 +104,7 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
/^(\+34|0034|34)?9\d{8}$/ // Landline /^(\+34|0034|34)?9\d{8}$/ // Landline
]; ];
if (!patterns.some(pattern => pattern.test(phone))) { if (!patterns.some(pattern => pattern.test(phone))) {
errors.phone = 'Introduce un número de teléfono español válido'; errors.phone = t('onboarding:child_tenants.validation.phone_format');
} }
} }
@@ -112,7 +112,7 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
if (formData.email && formData.email.trim()) { if (formData.email && formData.email.trim()) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(formData.email)) { if (!emailRegex.test(formData.email)) {
errors.email = 'Introduce un correo electrónico válido'; errors.email = t('onboarding:child_tenants.validation.email_format');
} }
} }
@@ -191,7 +191,7 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
const handleContinue = () => { const handleContinue = () => {
if (childTenants.length === 0) { if (childTenants.length === 0) {
alert('Debes agregar al menos una sucursal para continuar'); alert(t('onboarding:child_tenants.alerts.require_one'));
return; return;
} }
onComplete?.({ childTenants }); onComplete?.({ childTenants });
@@ -204,12 +204,11 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
<div className="flex items-center justify-center gap-3"> <div className="flex items-center justify-center gap-3">
<Building2 className="w-10 h-10 text-[var(--color-primary)]" /> <Building2 className="w-10 h-10 text-[var(--color-primary)]" />
<h1 className="text-2xl md:text-3xl font-bold text-[var(--text-primary)]"> <h1 className="text-2xl md:text-3xl font-bold text-[var(--text-primary)]">
Configuración de Sucursales {t('onboarding:child_tenants.title')}
</h1> </h1>
</div> </div>
<p className="text-base md:text-lg text-[var(--text-secondary)] max-w-2xl mx-auto"> <p className="text-base md:text-lg text-[var(--text-secondary)] max-w-2xl mx-auto">
Como empresa con tier Enterprise, tienes un obrador central y múltiples sucursales. {t('onboarding:child_tenants.subtitle')}
Agrega la información de cada sucursal que recibirá productos del obrador central.
</p> </p>
</div> </div>
@@ -221,11 +220,10 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<h4 className="font-semibold text-[var(--text-primary)]"> <h4 className="font-semibold text-[var(--text-primary)]">
Modelo de Negocio Enterprise {t('onboarding:child_tenants.info_box.title')}
</h4> </h4>
<p className="text-sm text-[var(--text-secondary)]"> <p className="text-sm text-[var(--text-secondary)]">
Tu obrador central se encargará de la producción, y las sucursales recibirán {t('onboarding:child_tenants.info_box.description')}
los productos terminados mediante transferencias internas optimizadas.
</p> </p>
</div> </div>
</div> </div>
@@ -235,7 +233,7 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-[var(--text-primary)]"> <h2 className="text-lg font-semibold text-[var(--text-primary)]">
Sucursales ({childTenants.length}) {t('onboarding:child_tenants.list.title')} ({childTenants.length})
</h2> </h2>
<Button <Button
onClick={() => handleOpenModal()} onClick={() => handleOpenModal()}
@@ -243,7 +241,7 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
size="md" size="md"
leftIcon={<Plus className="w-4 h-4" />} leftIcon={<Plus className="w-4 h-4" />}
> >
Agregar Sucursal {t('onboarding:child_tenants.list.add_button')}
</Button> </Button>
</div> </div>
@@ -255,10 +253,10 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
</div> </div>
<div> <div>
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2"> <h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
No hay sucursales agregadas {t('onboarding:child_tenants.list.empty_state.title')}
</h3> </h3>
<p className="text-sm text-[var(--text-secondary)] mb-4"> <p className="text-sm text-[var(--text-secondary)] mb-4">
Comienza agregando las sucursales que forman parte de tu red empresarial {t('onboarding:child_tenants.list.empty_state.description')}
</p> </p>
<Button <Button
onClick={() => handleOpenModal()} onClick={() => handleOpenModal()}
@@ -266,7 +264,7 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
size="lg" size="lg"
leftIcon={<Plus className="w-5 h-5" />} leftIcon={<Plus className="w-5 h-5" />}
> >
Agregar Primera Sucursal {t('onboarding:child_tenants.list.empty_state.button')}
</Button> </Button>
</div> </div>
</div> </div>
@@ -302,14 +300,14 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
<button <button
onClick={() => handleOpenModal(tenant)} onClick={() => handleOpenModal(tenant)}
className="p-2 hover:bg-[var(--bg-tertiary)] rounded-lg transition-colors" className="p-2 hover:bg-[var(--bg-tertiary)] rounded-lg transition-colors"
title="Editar" title={t('onboarding:child_tenants.card.edit')}
> >
<Edit2 className="w-4 h-4 text-[var(--text-secondary)]" /> <Edit2 className="w-4 h-4 text-[var(--text-secondary)]" />
</button> </button>
<button <button
onClick={() => handleDeleteTenant(tenant.id)} onClick={() => handleDeleteTenant(tenant.id)}
className="p-2 hover:bg-[var(--color-error)]/10 rounded-lg transition-colors" className="p-2 hover:bg-[var(--color-error)]/10 rounded-lg transition-colors"
title="Eliminar" title={t('onboarding:child_tenants.card.delete')}
> >
<Trash2 className="w-4 h-4 text-[var(--color-error)]" /> <Trash2 className="w-4 h-4 text-[var(--color-error)]" />
</button> </button>
@@ -345,7 +343,7 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
size="lg" size="lg"
className="w-full sm:w-auto sm:min-w-[200px]" className="w-full sm:w-auto sm:min-w-[200px]"
> >
Continuar con {childTenants.length} {childTenants.length === 1 ? 'Sucursal' : 'Sucursales'} {t('onboarding:child_tenants.continue_button', { count: childTenants.length })}
</Button> </Button>
</div> </div>
)} )}
@@ -357,7 +355,7 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
size="lg" size="lg"
> >
<ModalHeader <ModalHeader
title={editingTenant ? 'Editar Sucursal' : 'Agregar Sucursal'} title={t(editingTenant ? 'onboarding:child_tenants.modal.title_edit' : 'onboarding:child_tenants.modal.title_add')}
showCloseButton showCloseButton
onClose={handleCloseModal} onClose={handleCloseModal}
/> />
@@ -366,12 +364,12 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
{/* Name */} {/* Name */}
<div> <div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1"> <label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Nombre de la Sucursal * {t('onboarding:child_tenants.modal.fields.name')} *
</label> </label>
<Input <Input
value={formData.name || ''} value={formData.name || ''}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="ej. Madrid - Salamanca" placeholder={t('onboarding:child_tenants.modal.placeholders.name')}
error={formErrors.name} error={formErrors.name}
/> />
</div> </div>
@@ -379,48 +377,48 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
{/* Location Code */} {/* Location Code */}
<div> <div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1"> <label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Código de Ubicación * (máx. 10 caracteres) {t('onboarding:child_tenants.modal.fields.location_code')} * {t('onboarding:child_tenants.modal.fields.location_code_max')}
</label> </label>
<Input <Input
value={formData.location_code || ''} value={formData.location_code || ''}
onChange={(e) => setFormData({ ...formData, location_code: e.target.value.toUpperCase() })} onChange={(e) => setFormData({ ...formData, location_code: e.target.value.toUpperCase() })}
placeholder="ej. MAD, BCN, VAL" placeholder={t('onboarding:child_tenants.modal.placeholders.location_code')}
maxLength={10} maxLength={10}
error={formErrors.location_code} error={formErrors.location_code}
/> />
<p className="text-xs text-[var(--text-tertiary)] mt-1"> <p className="text-xs text-[var(--text-tertiary)] mt-1">
Un código corto para identificar esta ubicación {t('onboarding:child_tenants.modal.fields.location_code_help')}
</p> </p>
</div> </div>
{/* Business Type */} {/* Business Type */}
<div> <div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1"> <label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Tipo de Negocio {t('onboarding:child_tenants.modal.fields.business_type')}
</label> </label>
<Select <Select
value={formData.business_type || 'bakery'} value={formData.business_type || 'bakery'}
onChange={(e) => setFormData({ ...formData, business_type: e.target.value })} onChange={(e) => setFormData({ ...formData, business_type: e.target.value })}
> >
<option value="bakery">Panadería</option> <option value="bakery">{t('onboarding:child_tenants.modal.business_types.bakery')}</option>
<option value="coffee_shop">Cafetería</option> <option value="coffee_shop">{t('onboarding:child_tenants.modal.business_types.coffee_shop')}</option>
<option value="pastry_shop">Pastelería</option> <option value="pastry_shop">{t('onboarding:child_tenants.modal.business_types.pastry_shop')}</option>
<option value="restaurant">Restaurante</option> <option value="restaurant">{t('onboarding:child_tenants.modal.business_types.restaurant')}</option>
</Select> </Select>
</div> </div>
{/* Business Model */} {/* Business Model */}
<div> <div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1"> <label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Modelo de Negocio {t('onboarding:child_tenants.modal.fields.business_model')}
</label> </label>
<Select <Select
value={formData.business_model || 'retail_bakery'} value={formData.business_model || 'retail_bakery'}
onChange={(e) => setFormData({ ...formData, business_model: e.target.value })} onChange={(e) => setFormData({ ...formData, business_model: e.target.value })}
> >
<option value="retail_bakery">Panadería Minorista</option> <option value="retail_bakery">{t('onboarding:child_tenants.modal.business_models.retail_bakery')}</option>
<option value="central_baker_satellite">Obrador Central + Sucursales</option> <option value="central_baker_satellite">{t('onboarding:child_tenants.modal.business_models.central_baker_satellite')}</option>
<option value="hybrid_bakery">Modelo Híbrido</option> <option value="hybrid_bakery">{t('onboarding:child_tenants.modal.business_models.hybrid_bakery')}</option>
</Select> </Select>
</div> </div>
@@ -428,14 +426,14 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1"> <label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Teléfono {t('onboarding:child_tenants.modal.fields.phone')}
</label> </label>
<div className="relative"> <div className="relative">
<Phone className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-[var(--text-secondary)]" /> <Phone className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-[var(--text-secondary)]" />
<Input <Input
value={formData.phone || ''} value={formData.phone || ''}
onChange={(e) => setFormData({ ...formData, phone: e.target.value })} onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
placeholder="ej. +34 123 456 789" placeholder={t('onboarding:child_tenants.modal.placeholders.phone')}
error={formErrors.phone} error={formErrors.phone}
className="pl-10" className="pl-10"
/> />
@@ -443,14 +441,14 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1"> <label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Email {t('onboarding:child_tenants.modal.fields.email')}
</label> </label>
<div className="relative"> <div className="relative">
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-[var(--text-secondary)]" /> <Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-[var(--text-secondary)]" />
<Input <Input
value={formData.email || ''} value={formData.email || ''}
onChange={(e) => setFormData({ ...formData, email: e.target.value })} onChange={(e) => setFormData({ ...formData, email: e.target.value })}
placeholder="ej. contacto@panaderia.com" placeholder={t('onboarding:child_tenants.modal.placeholders.email')}
error={formErrors.email} error={formErrors.email}
className="pl-10" className="pl-10"
/> />
@@ -461,7 +459,7 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
{/* Timezone */} {/* Timezone */}
<div> <div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1"> <label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Zona Horaria {t('onboarding:child_tenants.modal.fields.timezone')}
</label> </label>
<div className="relative"> <div className="relative">
<Clock className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-[var(--text-secondary)]" /> <Clock className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-[var(--text-secondary)]" />
@@ -470,10 +468,10 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
onChange={(e) => setFormData({ ...formData, timezone: e.target.value })} onChange={(e) => setFormData({ ...formData, timezone: e.target.value })}
className="pl-10" className="pl-10"
> >
<option value="Europe/Madrid">Europe/Madrid (UTC+1/UTC+2)</option> <option value="Europe/Madrid">{t('onboarding:child_tenants.modal.timezones.europe_madrid')}</option>
<option value="Europe/Paris">Europe/Paris (UTC+1/UTC+2)</option> <option value="Europe/Paris">{t('onboarding:child_tenants.modal.timezones.europe_paris')}</option>
<option value="Europe/London">Europe/London (UTC+0/UTC+1)</option> <option value="Europe/London">{t('onboarding:child_tenants.modal.timezones.europe_london')}</option>
<option value="America/New_York">America/New_York (UTC-5/UTC-4)</option> <option value="America/New_York">{t('onboarding:child_tenants.modal.timezones.america_new_york')}</option>
</Select> </Select>
</div> </div>
</div> </div>
@@ -481,26 +479,26 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
{/* Zone */} {/* Zone */}
<div> <div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1"> <label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Zona / Barrio (opcional) {t('onboarding:child_tenants.modal.fields.zone')}
</label> </label>
<Input <Input
value={formData.zone || ''} value={formData.zone || ''}
onChange={(e) => setFormData({ ...formData, zone: e.target.value })} onChange={(e) => setFormData({ ...formData, zone: e.target.value })}
placeholder="ej. Salamanca, Chamberí, Centro" placeholder={t('onboarding:child_tenants.modal.placeholders.zone')}
/> />
<p className="text-xs text-[var(--text-tertiary)] mt-1"> <p className="text-xs text-[var(--text-tertiary)] mt-1">
Zona o barrio específico dentro de la ciudad {t('onboarding:child_tenants.modal.fields.zone_help')}
</p> </p>
</div> </div>
{/* Address with Autocomplete */} {/* Address with Autocomplete */}
<div> <div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1"> <label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Dirección * {t('onboarding:child_tenants.modal.fields.address')} *
</label> </label>
<AddressAutocomplete <AddressAutocomplete
value={formData.address || ''} value={formData.address || ''}
placeholder="ej. Calle de Serrano, 48" placeholder={t('onboarding:child_tenants.modal.placeholders.address')}
onAddressSelect={(address) => { onAddressSelect={(address) => {
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
@@ -532,23 +530,23 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1"> <label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Ciudad * {t('onboarding:child_tenants.modal.fields.city')} *
</label> </label>
<Input <Input
value={formData.city || ''} value={formData.city || ''}
onChange={(e) => setFormData({ ...formData, city: e.target.value })} onChange={(e) => setFormData({ ...formData, city: e.target.value })}
placeholder="ej. Madrid" placeholder={t('onboarding:child_tenants.modal.placeholders.city')}
error={formErrors.city} error={formErrors.city}
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1"> <label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
Código Postal * {t('onboarding:child_tenants.modal.fields.postal_code')} *
</label> </label>
<Input <Input
value={formData.postal_code || ''} value={formData.postal_code || ''}
onChange={(e) => setFormData({ ...formData, postal_code: e.target.value })} onChange={(e) => setFormData({ ...formData, postal_code: e.target.value })}
placeholder="ej. 28001" placeholder={t('onboarding:child_tenants.modal.placeholders.postal_code')}
error={formErrors.postal_code} error={formErrors.postal_code}
maxLength={5} maxLength={5}
/> />
@@ -559,10 +557,10 @@ export const ChildTenantsSetupStep: React.FC<ChildTenantSetupStepProps> = ({
<ModalFooter justify="end"> <ModalFooter justify="end">
<div className="flex gap-3"> <div className="flex gap-3">
<Button variant="outline" onClick={handleCloseModal}> <Button variant="outline" onClick={handleCloseModal}>
Cancelar {t('onboarding:child_tenants.modal.buttons.cancel')}
</Button> </Button>
<Button variant="primary" onClick={handleSaveTenant}> <Button variant="primary" onClick={handleSaveTenant}>
{editingTenant ? 'Guardar Cambios' : 'Agregar Sucursal'} {t(editingTenant ? 'onboarding:child_tenants.modal.buttons.save' : 'onboarding:child_tenants.modal.buttons.add')}
</Button> </Button>
</div> </div>
</ModalFooter> </ModalFooter>

View File

@@ -63,18 +63,18 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
setTrainingProgress({ setTrainingProgress({
stage: 'training', stage: 'training',
progress: data.data?.progress || 0, progress: data.data?.progress || 0,
message: data.data?.message || 'Entrenando modelo...', message: data.data?.message || t('onboarding:steps.ml_training.messages.training'),
currentStep: data.data?.current_step, currentStep: data.data?.current_step,
estimatedTimeRemaining: data.data?.estimated_time_remaining_seconds || data.data?.estimated_time_remaining, estimatedTimeRemaining: data.data?.estimated_time_remaining_seconds || data.data?.estimated_time_remaining,
estimatedCompletionTime: data.data?.estimated_completion_time estimatedCompletionTime: data.data?.estimated_completion_time
}); });
}, []); }, [t]);
const handleCompleted = useCallback((_data: any) => { const handleCompleted = useCallback((_data: any) => {
setTrainingProgress({ setTrainingProgress({
stage: 'completed', stage: 'completed',
progress: 100, progress: 100,
message: 'Entrenamiento completado exitosamente' message: t('onboarding:steps.ml_training.messages.completed')
}); });
setIsTraining(false); setIsTraining(false);
@@ -82,24 +82,24 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
onComplete({ onComplete({
jobId: jobId, jobId: jobId,
success: true, success: true,
message: 'Modelo entrenado correctamente' message: t('onboarding:steps.ml_training.messages.completed')
}); });
}, TRAINING_COMPLETION_DELAY_MS); }, TRAINING_COMPLETION_DELAY_MS);
}, [onComplete, jobId]); }, [onComplete, jobId, t]);
const handleError = useCallback((data: any) => { const handleError = useCallback((data: any) => {
setError(data.data?.error || data.error || 'Error durante el entrenamiento'); setError(data.data?.error || data.error || t('onboarding:errors.training_failed'));
setIsTraining(false); setIsTraining(false);
setTrainingProgress(null); setTrainingProgress(null);
}, []); }, [t]);
const handleStarted = useCallback((_data: any) => { const handleStarted = useCallback((_data: any) => {
setTrainingProgress({ setTrainingProgress({
stage: 'starting', stage: 'starting',
progress: 5, progress: 5,
message: 'Iniciando entrenamiento del modelo...' message: t('onboarding:steps.ml_training.messages.starting')
}); });
}, []); }, [t]);
// WebSocket for real-time training progress - only connect when we have a jobId // WebSocket for real-time training progress - only connect when we have a jobId
const { isConnected, connectionError } = useTrainingWebSocket( const { isConnected, connectionError } = useTrainingWebSocket(
@@ -140,8 +140,8 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
stage: 'completed', stage: 'completed',
progress: 100, progress: 100,
message: isConnected message: isConnected
? 'Entrenamiento completado exitosamente' ? t('onboarding:steps.ml_training.messages.completed')
: 'Entrenamiento completado exitosamente (detectado por verificación HTTP)' : t('onboarding:steps.ml_training.messages.completed_http')
}); });
setIsTraining(false); setIsTraining(false);
@@ -149,13 +149,13 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
onComplete({ onComplete({
jobId: jobId, jobId: jobId,
success: true, success: true,
message: 'Modelo entrenado correctamente', message: t('onboarding:steps.ml_training.messages.completed'),
detectedViaPolling: true detectedViaPolling: true
}); });
}, TRAINING_COMPLETION_DELAY_MS); }, TRAINING_COMPLETION_DELAY_MS);
} else if (jobStatus.status === 'failed') { } else if (jobStatus.status === 'failed') {
console.log(`❌ Training failure detected (source: ${isConnected ? 'WebSocket' : 'HTTP polling'})`); console.log(`❌ Training failure detected (source: ${isConnected ? 'WebSocket' : 'HTTP polling'})`);
setError('Error detectado durante el entrenamiento (verificación de estado)'); setError(t('onboarding:errors.training_failed'));
setIsTraining(false); setIsTraining(false);
setTrainingProgress(null); setTrainingProgress(null);
} else if (jobStatus.status === 'running' && jobStatus.progress !== undefined) { } else if (jobStatus.status === 'running' && jobStatus.progress !== undefined) {
@@ -167,12 +167,12 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
...prev, ...prev,
stage: 'training', stage: 'training',
progress: jobStatus.progress, progress: jobStatus.progress,
message: jobStatus.message || 'Entrenando modelo...', message: jobStatus.message || t('onboarding:steps.ml_training.messages.training'),
currentStep: jobStatus.current_step currentStep: jobStatus.current_step
}) as TrainingProgress); }) as TrainingProgress);
} }
} }
}, [jobStatus, jobId, trainingProgress?.stage, onComplete, isConnected]); }, [jobStatus, jobId, trainingProgress?.stage, onComplete, isConnected, t]);
// Auto-trigger training when component mounts (run once) // Auto-trigger training when component mounts (run once)
const hasAutoStarted = React.useRef(false); const hasAutoStarted = React.useRef(false);
@@ -186,7 +186,7 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
const handleStartTraining = async () => { const handleStartTraining = async () => {
if (!currentTenant?.id) { if (!currentTenant?.id) {
setError('No se encontró información del tenant'); setError(t('onboarding:errors.network_error'));
return; return;
} }
@@ -195,7 +195,7 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
setTrainingProgress({ setTrainingProgress({
stage: 'preparing', stage: 'preparing',
progress: 0, progress: 0,
message: 'Preparando datos para entrenamiento...' message: t('onboarding:steps.ml_training.messages.preparing')
}); });
try { try {
@@ -213,10 +213,10 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
setTrainingProgress({ setTrainingProgress({
stage: 'queued', stage: 'queued',
progress: 10, progress: 10,
message: 'Trabajo de entrenamiento en cola...' message: t('onboarding:steps.ml_training.messages.queued')
}); });
} catch (err) { } catch (err) {
setError('Error al iniciar el entrenamiento del modelo'); setError(t('onboarding:errors.training_failed'));
setIsTraining(false); setIsTraining(false);
setTrainingProgress(null); setTrainingProgress(null);
} }
@@ -271,8 +271,7 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
<div className="space-y-6"> <div className="space-y-6">
<div className="text-center"> <div className="text-center">
<p className="text-[var(--text-secondary)] mb-6"> <p className="text-[var(--text-secondary)] mb-6">
Perfecto! Ahora entrenaremos automáticamente tu modelo de inteligencia artificial utilizando los datos de ventas {t('onboarding:steps.ml_training.intro_text')}
e inventario que has proporcionado. Este proceso puede tomar varios minutos.
</p> </p>
</div> </div>
@@ -285,9 +284,9 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[var(--color-primary)]"></div> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[var(--color-primary)]"></div>
</div> </div>
<div> <div>
<h3 className="text-lg font-semibold mb-2">Iniciando Entrenamiento Automático</h3> <h3 className="text-lg font-semibold mb-2">{t('onboarding:steps.ml_training.status.preparing')}</h3>
<p className="text-[var(--text-secondary)] text-sm"> <p className="text-[var(--text-secondary)] text-sm">
Preparando el entrenamiento de tu modelo con los datos proporcionados... {t('onboarding:steps.ml_training.messages.preparing')}
</p> </p>
</div> </div>
</div> </div>
@@ -319,9 +318,9 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
<div> <div>
<h3 className="text-lg font-semibold mb-2"> <h3 className="text-lg font-semibold mb-2">
{trainingProgress.stage === 'completed' {trainingProgress.stage === 'completed'
? '¡Entrenamiento Completo!' ? t('onboarding:steps.ml_training.status.completed')
: 'Entrenando Modelo IA' : t('onboarding:steps.ml_training.title')
} }
</h3> </h3>
<p className="text-[var(--text-secondary)] text-sm mb-4"> <p className="text-[var(--text-secondary)] text-sm mb-4">
@@ -356,7 +355,7 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
<span className="font-medium">{trainingProgress.currentStep || t('onboarding:steps.ml_training.progress.data_preparation', 'Procesando...')}</span> <span className="font-medium">{trainingProgress.currentStep || t('onboarding:steps.ml_training.progress.data_preparation', 'Procesando...')}</span>
{jobId && ( {jobId && (
<span className={`text-xs px-2 py-0.5 rounded-full ${isConnected ? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400' : 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'}`}> <span className={`text-xs px-2 py-0.5 rounded-full ${isConnected ? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400' : 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'}`}>
{isConnected ? '● En vivo' : '● Reconectando...'} {isConnected ? `${t('onboarding:steps.ml_training.messages.live')}` : `${t('onboarding:steps.ml_training.messages.reconnecting')}`}
</span> </span>
)} )}
</div> </div>
@@ -381,7 +380,7 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg> </svg>
<span> <span>
Finalizará: {formatEstimatedCompletionTime(trainingProgress.estimatedCompletionTime)} {t('onboarding:steps.ml_training.messages.estimated_completion')} {formatEstimatedCompletionTime(trainingProgress.estimatedCompletionTime)}
</span> </span>
</div> </div>
)} )}
@@ -426,12 +425,12 @@ export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
{/* Training Info */} {/* Training Info */}
<div className="bg-[var(--bg-secondary)] rounded-lg p-4"> <div className="bg-[var(--bg-secondary)] rounded-lg p-4">
<h4 className="font-medium mb-2">¿Qué sucede durante el entrenamiento?</h4> <h4 className="font-medium mb-2">{t('onboarding:steps.ml_training.training_info.title')}</h4>
<ul className="text-sm text-[var(--text-secondary)] space-y-1"> <ul className="text-sm text-[var(--text-secondary)] space-y-1">
<li> Análisis de patrones de ventas históricos</li> <li> {t('onboarding:steps.ml_training.training_info.step1')}</li>
<li> Creación de modelos predictivos de demanda</li> <li> {t('onboarding:steps.ml_training.training_info.step2')}</li>
<li> Optimización de algoritmos de inventario</li> <li> {t('onboarding:steps.ml_training.training_info.step3')}</li>
<li> Validación y ajuste de precisión</li> <li> {t('onboarding:steps.ml_training.training_info.step4')}</li>
</ul> </ul>
</div> </div>

View File

@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Input } from '../../../ui'; import { Button, Input } from '../../../ui';
import { AddressAutocomplete } from '../../../ui/AddressAutocomplete'; import { AddressAutocomplete } from '../../../ui/AddressAutocomplete';
import { useRegisterBakery, useTenant, useUpdateTenant } from '../../../../api/hooks/tenant'; import { useRegisterBakery, useTenant, useUpdateTenant } from '../../../../api/hooks/tenant';
@@ -33,6 +34,7 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
onComplete, onComplete,
isFirstStep isFirstStep
}) => { }) => {
const { t } = useTranslation();
const wizardContext = useWizardContext(); const wizardContext = useWizardContext();
const tenantId = wizardContext.state.tenantId; const tenantId = wizardContext.state.tenantId;
@@ -126,27 +128,27 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
// Required fields according to backend BakeryRegistration schema // Required fields according to backend BakeryRegistration schema
if (!formData.name.trim()) { if (!formData.name.trim()) {
newErrors.name = 'El nombre de la panadería es obligatorio'; newErrors.name = t('onboarding:steps.tenant_registration.validation.name_required');
} else if (formData.name.length < 2 || formData.name.length > 200) { } else if (formData.name.length < 2 || formData.name.length > 200) {
newErrors.name = 'El nombre debe tener entre 2 y 200 caracteres'; newErrors.name = t('onboarding:steps.tenant_registration.validation.name_length');
} }
if (!formData.address.trim()) { if (!formData.address.trim()) {
newErrors.address = 'La dirección es obligatoria'; newErrors.address = t('onboarding:steps.tenant_registration.validation.address_required');
} else if (formData.address.length < 10 || formData.address.length > 500) { } else if (formData.address.length < 10 || formData.address.length > 500) {
newErrors.address = 'La dirección debe tener entre 10 y 500 caracteres'; newErrors.address = t('onboarding:steps.tenant_registration.validation.address_length');
} }
if (!formData.postal_code.trim()) { if (!formData.postal_code.trim()) {
newErrors.postal_code = 'El código postal es obligatorio'; newErrors.postal_code = t('onboarding:steps.tenant_registration.validation.postal_code_required');
} else if (!/^\d{5}$/.test(formData.postal_code)) { } else if (!/^\d{5}$/.test(formData.postal_code)) {
newErrors.postal_code = 'El código postal debe tener exactamente 5 dígitos'; newErrors.postal_code = t('onboarding:steps.tenant_registration.validation.postal_code_format');
} }
if (!formData.phone.trim()) { if (!formData.phone.trim()) {
newErrors.phone = 'El número de teléfono es obligatorio'; newErrors.phone = t('onboarding:steps.tenant_registration.validation.phone_required');
} else if (formData.phone.length < 9 || formData.phone.length > 20) { } else if (formData.phone.length < 9 || formData.phone.length > 20) {
newErrors.phone = 'El teléfono debe tener entre 9 y 20 caracteres'; newErrors.phone = t('onboarding:steps.tenant_registration.validation.phone_length');
} else { } else {
// Basic Spanish phone validation // Basic Spanish phone validation
const phone = formData.phone.replace(/[\s\-\(\)]/g, ''); const phone = formData.phone.replace(/[\s\-\(\)]/g, '');
@@ -155,7 +157,7 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
/^(\+34|0034|34)?9\d{8}$/ // Landline /^(\+34|0034|34)?9\d{8}$/ // Landline
]; ];
if (!patterns.some(pattern => pattern.test(phone))) { if (!patterns.some(pattern => pattern.test(phone))) {
newErrors.phone = 'Introduce un número de teléfono español válido'; newErrors.phone = t('onboarding:steps.tenant_registration.validation.phone_format');
} }
} }
@@ -240,7 +242,7 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
}); });
} catch (error) { } catch (error) {
console.error('Error registering bakery:', error); console.error('Error registering bakery:', error);
setErrors({ submit: 'Error al registrar la panadería. Por favor, inténtalo de nuevo.' }); setErrors({ submit: t('onboarding:steps.tenant_registration.errors.register') });
} }
}; };
@@ -252,12 +254,16 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
<div className="text-2xl flex-shrink-0">{isEnterprise ? '🏭' : '🏪'}</div> <div className="text-2xl flex-shrink-0">{isEnterprise ? '🏭' : '🏪'}</div>
<div> <div>
<h3 className="font-semibold text-[var(--text-primary)] mb-1"> <h3 className="font-semibold text-[var(--text-primary)] mb-1">
{isEnterprise ? 'Registra tu Obrador Central' : 'Registra tu Panadería'} {t(isEnterprise
? 'onboarding:steps.tenant_registration.header.title_enterprise'
: 'onboarding:steps.tenant_registration.header.title'
)}
</h3> </h3>
<p className="text-sm text-[var(--text-secondary)]"> <p className="text-sm text-[var(--text-secondary)]">
{isEnterprise {t(isEnterprise
? 'Ingresa los datos de tu obrador principal. Después podrás agregar las sucursales.' ? 'onboarding:steps.tenant_registration.header.description_enterprise'
: 'Completa la información básica de tu panadería para comenzar.'} : 'onboarding:steps.tenant_registration.header.description'
)}
</p> </p>
</div> </div>
</div> </div>
@@ -266,8 +272,14 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
<div className="grid grid-cols-1 md:grid-cols-2 gap-5 md:gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-5 md:gap-6">
<div className="transform transition-all duration-200 hover:scale-[1.01]"> <div className="transform transition-all duration-200 hover:scale-[1.01]">
<Input <Input
label={isEnterprise ? "Nombre del Obrador Central" : "Nombre de la Panadería"} label={t(isEnterprise
placeholder={isEnterprise ? "Ingresa el nombre de tu obrador central" : "Ingresa el nombre de tu panadería"} ? 'onboarding:steps.tenant_registration.fields.business_name_enterprise'
: 'onboarding:steps.tenant_registration.fields.business_name'
)}
placeholder={t(isEnterprise
? 'onboarding:steps.tenant_registration.placeholders.business_name_enterprise'
: 'onboarding:steps.tenant_registration.placeholders.business_name'
)}
value={formData.name} value={formData.name}
onChange={(e) => handleInputChange('name', e.target.value)} onChange={(e) => handleInputChange('name', e.target.value)}
error={errors.name} error={errors.name}
@@ -277,9 +289,9 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
<div className="transform transition-all duration-200 hover:scale-[1.01]"> <div className="transform transition-all duration-200 hover:scale-[1.01]">
<Input <Input
label="Teléfono" label={t('onboarding:steps.tenant_registration.fields.phone')}
type="tel" type="tel"
placeholder="+34 123 456 789" placeholder={t('onboarding:steps.tenant_registration.placeholders.phone')}
value={formData.phone} value={formData.phone}
onChange={(e) => handleInputChange('phone', e.target.value)} onChange={(e) => handleInputChange('phone', e.target.value)}
error={errors.phone} error={errors.phone}
@@ -290,11 +302,14 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
<div className="md:col-span-2 transform transition-all duration-200 hover:scale-[1.01] relative z-20"> <div className="md:col-span-2 transform transition-all duration-200 hover:scale-[1.01] relative z-20">
<label className="block text-sm font-semibold text-[var(--text-primary)] mb-2 flex items-center gap-2"> <label className="block text-sm font-semibold text-[var(--text-primary)] mb-2 flex items-center gap-2">
<span className="text-lg">📍</span> <span className="text-lg">📍</span>
Dirección <span className="text-red-500">*</span> {t('onboarding:steps.tenant_registration.fields.address')} <span className="text-red-500">*</span>
</label> </label>
<AddressAutocomplete <AddressAutocomplete
value={formData.address} value={formData.address}
placeholder={isEnterprise ? "Dirección del obrador central..." : "Dirección de tu panadería..."} placeholder={t(isEnterprise
? 'onboarding:steps.tenant_registration.placeholders.address_enterprise'
: 'onboarding:steps.tenant_registration.placeholders.address'
)}
onAddressSelect={(address) => { onAddressSelect={(address) => {
console.log('Selected:', address.display_name); console.log('Selected:', address.display_name);
handleAddressSelect(address); handleAddressSelect(address);
@@ -313,8 +328,8 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
<div className="transform transition-all duration-200 hover:scale-[1.01]"> <div className="transform transition-all duration-200 hover:scale-[1.01]">
<Input <Input
label="Código Postal" label={t('onboarding:steps.tenant_registration.fields.postal_code')}
placeholder="28001" placeholder={t('onboarding:steps.tenant_registration.placeholders.postal_code')}
value={formData.postal_code} value={formData.postal_code}
onChange={(e) => handleInputChange('postal_code', e.target.value)} onChange={(e) => handleInputChange('postal_code', e.target.value)}
error={errors.postal_code} error={errors.postal_code}
@@ -325,8 +340,8 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
<div className="transform transition-all duration-200 hover:scale-[1.01]"> <div className="transform transition-all duration-200 hover:scale-[1.01]">
<Input <Input
label="Ciudad (Opcional)" label={t('onboarding:steps.tenant_registration.fields.city')}
placeholder="Madrid" placeholder={t('onboarding:steps.tenant_registration.placeholders.city')}
value={formData.city} value={formData.city}
onChange={(e) => handleInputChange('city', e.target.value)} onChange={(e) => handleInputChange('city', e.target.value)}
error={errors.city} error={errors.city}
@@ -339,7 +354,9 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<span className="text-2xl flex-shrink-0"></span> <span className="text-2xl flex-shrink-0"></span>
<div> <div>
<h4 className="font-semibold text-[var(--color-error)] mb-1">Error al registrar</h4> <h4 className="font-semibold text-[var(--color-error)] mb-1">
{t('onboarding:steps.tenant_registration.errors.register_title')}
</h4>
<p className="text-sm text-[var(--text-secondary)]">{errors.submit}</p> <p className="text-sm text-[var(--text-secondary)]">{errors.submit}</p>
</div> </div>
</div> </div>
@@ -350,14 +367,26 @@ export const RegisterTenantStep: React.FC<RegisterTenantStepProps> = ({
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
isLoading={registerBakery.isPending || updateTenant.isPending} isLoading={registerBakery.isPending || updateTenant.isPending}
loadingText={tenantId ? "Actualizando obrador..." : (isEnterprise ? "Registrando obrador..." : "Registrando...")} loadingText={t(tenantId
? 'onboarding:steps.tenant_registration.loading.updating'
: (isEnterprise
? 'onboarding:steps.tenant_registration.loading.creating_enterprise'
: 'onboarding:steps.tenant_registration.loading.creating'
)
)}
size="lg" size="lg"
className="w-full sm:w-auto sm:min-w-[280px] text-base font-semibold transform transition-all duration-300 hover:scale-105 shadow-lg" className="w-full sm:w-auto sm:min-w-[280px] text-base font-semibold transform transition-all duration-300 hover:scale-105 shadow-lg"
> >
{tenantId {t(tenantId
? (isEnterprise ? "Actualizar Obrador Central y Continuar →" : "Actualizar Panadería y Continuar →") ? (isEnterprise
: (isEnterprise ? "Crear Obrador Central y Continuar →" : "Crear Panadería y Continuar →") ? 'onboarding:steps.tenant_registration.buttons.update_enterprise'
} : 'onboarding:steps.tenant_registration.buttons.update'
)
: (isEnterprise
? 'onboarding:steps.tenant_registration.buttons.create_enterprise'
: 'onboarding:steps.tenant_registration.buttons.create'
)
)}
</Button> </Button>
</div> </div>
</div> </div>

View File

@@ -79,23 +79,63 @@
"steps": { "steps": {
"tenant_registration": { "tenant_registration": {
"title": "Your Bakery Information", "title": "Your Bakery Information",
"title_enterprise": "Your Central Bakery Information",
"subtitle": "Tell us about your business", "subtitle": "Tell us about your business",
"header": {
"title": "Register Your Bakery",
"title_enterprise": "Register Your Central Bakery",
"description": "Complete your bakery's basic information to get started.",
"description_enterprise": "Enter your main production facility details. You can add branches afterwards."
},
"fields": { "fields": {
"business_name": "Business name", "business_name": "Bakery Name",
"business_name_enterprise": "Central Bakery Name",
"business_type": "Business type", "business_type": "Business type",
"address": "Address", "address": "Address",
"phone": "Phone", "phone": "Phone",
"postal_code": "Postal Code",
"city": "City (Optional)",
"email": "Contact email", "email": "Contact email",
"website": "Website (optional)", "website": "Website (optional)",
"description": "Business description" "description": "Business description"
}, },
"placeholders": { "placeholders": {
"business_name": "E.g: San José Bakery", "business_name": "Enter your bakery name",
"address": "Main Street 123, City", "business_name_enterprise": "Enter your central bakery name",
"phone": "+1 123 456 789", "address": "Your bakery address...",
"address_enterprise": "Central bakery address...",
"phone": "+34 123 456 789",
"postal_code": "28001",
"city": "Madrid",
"email": "contact@bakery.com", "email": "contact@bakery.com",
"website": "https://mybakery.com", "website": "https://mybakery.com",
"description": "Describe your bakery..." "description": "Describe your bakery..."
},
"validation": {
"name_required": "Bakery name is required",
"name_length": "Name must be between 2 and 200 characters",
"address_required": "Address is required",
"address_length": "Address must be between 10 and 500 characters",
"postal_code_required": "Postal code is required",
"postal_code_format": "Postal code must be exactly 5 digits",
"phone_required": "Phone number is required",
"phone_length": "Phone must be between 9 and 20 characters",
"phone_format": "Please enter a valid Spanish phone number"
},
"buttons": {
"create": "Create Bakery and Continue →",
"create_enterprise": "Create Central Bakery and Continue →",
"update": "Update Bakery and Continue →",
"update_enterprise": "Update Central Bakery and Continue →"
},
"loading": {
"creating": "Creating...",
"creating_enterprise": "Creating bakery...",
"updating": "Updating bakery..."
},
"errors": {
"register": "Error registering bakery. Please try again.",
"register_title": "Registration Error"
} }
}, },
"inventory_setup": { "inventory_setup": {
@@ -169,6 +209,7 @@
"ml_training": { "ml_training": {
"title": "AI Training", "title": "AI Training",
"subtitle": "Creating your personalized model", "subtitle": "Creating your personalized model",
"intro_text": "Perfect! We will now automatically train your artificial intelligence model using the sales and inventory data you've provided. This process may take several minutes.",
"status": { "status": {
"preparing": "Preparing data...", "preparing": "Preparing data...",
"training": "Training model...", "training": "Training model...",
@@ -179,11 +220,30 @@
"data_preparation": "Data preparation", "data_preparation": "Data preparation",
"model_training": "Model training", "model_training": "Model training",
"validation": "Validation", "validation": "Validation",
"deployment": "Deployment" "deployment": "Deployment",
"processing": "Processing..."
}, },
"estimated_time": "Estimated time: {{minutes}} minutes", "estimated_time": "Estimated time: {{minutes}} minutes",
"estimated_time_remaining": "Estimated time remaining: {{time}}", "estimated_time_remaining": "Estimated time remaining: {{time}}",
"description": "We're creating a personalized AI model for your bakery based on your historical data.", "description": "We're creating a personalized AI model for your bakery based on your historical data.",
"training_info": {
"title": "What happens during training?",
"step1": "Analysis of historical sales patterns",
"step2": "Creation of demand prediction models",
"step3": "Optimization of inventory algorithms",
"step4": "Validation and accuracy adjustment"
},
"messages": {
"training": "Training model...",
"completed": "Training completed successfully",
"completed_http": "Training completed successfully (detected via HTTP check)",
"starting": "Starting model training...",
"queued": "Training job queued...",
"preparing": "Preparing training data...",
"live": "Live",
"reconnecting": "Reconnecting...",
"estimated_completion": "Will finish:"
},
"skip_to_dashboard": { "skip_to_dashboard": {
"title": "Taking too long?", "title": "Taking too long?",
"description": "Training continues in the background. You can go to the dashboard now and explore your system while the model finishes training.", "description": "Training continues in the background. You can go to the dashboard now and explore your system while the model finishes training.",
@@ -298,5 +358,94 @@
"continue_anyway": "Continue anyway", "continue_anyway": "Continue anyway",
"no_products_title": "Initial Stock", "no_products_title": "Initial Stock",
"no_products_message": "You can configure stock levels later in the inventory section." "no_products_message": "You can configure stock levels later in the inventory section."
},
"child_tenants": {
"title": "Branch Configuration",
"subtitle": "As an Enterprise tier company, you have a central bakery and multiple branches. Add information for each branch that will receive products from the central bakery.",
"info_box": {
"title": "Enterprise Business Model",
"description": "Your central bakery handles production, and branches receive finished products through optimized internal transfers."
},
"list": {
"title": "Branches",
"add_button": "Add Branch",
"empty_state": {
"title": "No branches added",
"description": "Start by adding the branches that are part of your enterprise network",
"button": "Add First Branch"
}
},
"modal": {
"title_add": "Add Branch",
"title_edit": "Edit Branch",
"fields": {
"name": "Branch Name",
"location_code": "Location Code",
"location_code_help": "A short code to identify this location",
"location_code_max": "(max 10 characters)",
"business_type": "Business Type",
"business_model": "Business Model",
"phone": "Phone",
"email": "Email",
"timezone": "Timezone",
"zone": "Zone / District (optional)",
"zone_help": "Specific zone or district within the city",
"address": "Address",
"city": "City",
"postal_code": "Postal Code"
},
"placeholders": {
"name": "e.g. Madrid - Salamanca",
"location_code": "e.g. MAD, BCN, VAL",
"phone": "e.g. +34 123 456 789",
"email": "e.g. contact@bakery.com",
"zone": "e.g. Salamanca, Downtown, Center",
"address": "e.g. Serrano Street, 48",
"city": "e.g. Madrid",
"postal_code": "e.g. 28001"
},
"business_types": {
"bakery": "Bakery",
"coffee_shop": "Coffee Shop",
"pastry_shop": "Pastry Shop",
"restaurant": "Restaurant"
},
"business_models": {
"retail_bakery": "Retail Bakery",
"central_baker_satellite": "Central Baker + Branches",
"hybrid_bakery": "Hybrid Model"
},
"timezones": {
"europe_madrid": "Europe/Madrid (UTC+1/UTC+2)",
"europe_paris": "Europe/Paris (UTC+1/UTC+2)",
"europe_london": "Europe/London (UTC+0/UTC+1)",
"america_new_york": "America/New_York (UTC-5/UTC-4)"
},
"buttons": {
"cancel": "Cancel",
"save": "Save Changes",
"add": "Add Branch"
}
},
"card": {
"edit": "Edit",
"delete": "Delete"
},
"continue_button": "Continue with {count} {count, plural, one {Branch} other {Branches}}",
"validation": {
"name_required": "Name is required",
"city_required": "City is required",
"address_required": "Address is required",
"postal_code_required": "Postal code is required",
"postal_code_format": "Postal code must be exactly 5 digits",
"location_code_required": "Location code is required",
"location_code_max": "Code must not exceed 10 characters",
"location_code_format": "Only uppercase letters, numbers, and hyphens/underscores are allowed",
"phone_format": "Please enter a valid Spanish phone number",
"email_format": "Please enter a valid email address"
},
"alerts": {
"require_one": "You must add at least one branch to continue"
}
} }
} }

View File

@@ -100,23 +100,63 @@
"steps": { "steps": {
"tenant_registration": { "tenant_registration": {
"title": "Información de tu Panadería", "title": "Información de tu Panadería",
"title_enterprise": "Información de tu Obrador Central",
"subtitle": "Cuéntanos sobre tu negocio", "subtitle": "Cuéntanos sobre tu negocio",
"header": {
"title": "Registra tu Panadería",
"title_enterprise": "Registra tu Obrador Central",
"description": "Completa la información básica de tu panadería para comenzar.",
"description_enterprise": "Ingresa los datos de tu obrador principal. Después podrás agregar las sucursales."
},
"fields": { "fields": {
"business_name": "Nombre del negocio", "business_name": "Nombre de la Panadería",
"business_name_enterprise": "Nombre del Obrador Central",
"business_type": "Tipo de negocio", "business_type": "Tipo de negocio",
"address": "Dirección", "address": "Dirección",
"phone": "Teléfono", "phone": "Teléfono",
"postal_code": "Código Postal",
"city": "Ciudad (Opcional)",
"email": "Email de contacto", "email": "Email de contacto",
"website": "Sitio web (opcional)", "website": "Sitio web (opcional)",
"description": "Descripción del negocio" "description": "Descripción del negocio"
}, },
"placeholders": { "placeholders": {
"business_name": "Ej: Panadería San José", "business_name": "Ingresa el nombre de tu panadería",
"address": "Calle Principal 123, Ciudad", "business_name_enterprise": "Ingresa el nombre de tu obrador central",
"address": "Dirección de tu panadería...",
"address_enterprise": "Dirección del obrador central...",
"phone": "+34 123 456 789", "phone": "+34 123 456 789",
"postal_code": "28001",
"city": "Madrid",
"email": "contacto@panaderia.com", "email": "contacto@panaderia.com",
"website": "https://mipanaderia.com", "website": "https://mipanaderia.com",
"description": "Describe tu panadería..." "description": "Describe tu panadería..."
},
"validation": {
"name_required": "El nombre de la panadería es obligatorio",
"name_length": "El nombre debe tener entre 2 y 200 caracteres",
"address_required": "La dirección es obligatoria",
"address_length": "La dirección debe tener entre 10 y 500 caracteres",
"postal_code_required": "El código postal es obligatorio",
"postal_code_format": "El código postal debe tener exactamente 5 dígitos",
"phone_required": "El número de teléfono es obligatorio",
"phone_length": "El teléfono debe tener entre 9 y 20 caracteres",
"phone_format": "Introduce un número de teléfono español válido"
},
"buttons": {
"create": "Crear Panadería y Continuar →",
"create_enterprise": "Crear Obrador Central y Continuar →",
"update": "Actualizar Panadería y Continuar →",
"update_enterprise": "Actualizar Obrador Central y Continuar →"
},
"loading": {
"creating": "Registrando...",
"creating_enterprise": "Registrando obrador...",
"updating": "Actualizando obrador..."
},
"errors": {
"register": "Error al registrar la panadería. Por favor, inténtalo de nuevo.",
"register_title": "Error al registrar"
} }
}, },
"inventory_setup": { "inventory_setup": {
@@ -190,6 +230,7 @@
"ml_training": { "ml_training": {
"title": "Entrenamiento de IA", "title": "Entrenamiento de IA",
"subtitle": "Creando tu modelo personalizado", "subtitle": "Creando tu modelo personalizado",
"intro_text": "Perfecto! Ahora entrenaremos automáticamente tu modelo de inteligencia artificial utilizando los datos de ventas e inventario que has proporcionado. Este proceso puede tomar varios minutos.",
"status": { "status": {
"preparing": "Preparando datos...", "preparing": "Preparando datos...",
"training": "Entrenando modelo...", "training": "Entrenando modelo...",
@@ -200,11 +241,30 @@
"data_preparation": "Preparación de datos", "data_preparation": "Preparación de datos",
"model_training": "Entrenamiento del modelo", "model_training": "Entrenamiento del modelo",
"validation": "Validación", "validation": "Validación",
"deployment": "Despliegue" "deployment": "Despliegue",
"processing": "Procesando..."
}, },
"estimated_time": "Tiempo estimado: {{minutes}} minutos", "estimated_time": "Tiempo estimado: {{minutes}} minutos",
"estimated_time_remaining": "Tiempo restante estimado: {{time}}", "estimated_time_remaining": "Tiempo restante estimado: {{time}}",
"description": "Estamos creando un modelo de IA personalizado para tu panadería basado en tus datos históricos.", "description": "Estamos creando un modelo de IA personalizado para tu panadería basado en tus datos históricos.",
"training_info": {
"title": "¿Qué sucede durante el entrenamiento?",
"step1": "Análisis de patrones de ventas históricos",
"step2": "Creación de modelos predictivos de demanda",
"step3": "Optimización de algoritmos de inventario",
"step4": "Validación y ajuste de precisión"
},
"messages": {
"training": "Entrenando modelo...",
"completed": "Entrenamiento completado exitosamente",
"completed_http": "Entrenamiento completado exitosamente (detectado por verificación HTTP)",
"starting": "Iniciando entrenamiento del modelo...",
"queued": "Trabajo de entrenamiento en cola...",
"preparing": "Preparando datos para entrenamiento...",
"live": "En vivo",
"reconnecting": "Reconectando...",
"estimated_completion": "Finalizará:"
},
"skip_to_dashboard": { "skip_to_dashboard": {
"title": "¿Toma demasiado tiempo?", "title": "¿Toma demasiado tiempo?",
"description": "El entrenamiento continúa en segundo plano. Puedes ir al dashboard ahora y explorar tu sistema mientras el modelo termina de entrenarse.", "description": "El entrenamiento continúa en segundo plano. Puedes ir al dashboard ahora y explorar tu sistema mientras el modelo termina de entrenarse.",
@@ -420,5 +480,94 @@
"continue_anyway": "Continuar de todos modos", "continue_anyway": "Continuar de todos modos",
"no_products_title": "Stock Inicial", "no_products_title": "Stock Inicial",
"no_products_message": "Podrás configurar los niveles de stock más tarde en la sección de inventario." "no_products_message": "Podrás configurar los niveles de stock más tarde en la sección de inventario."
},
"child_tenants": {
"title": "Configuración de Sucursales",
"subtitle": "Como empresa con tier Enterprise, tienes un obrador central y múltiples sucursales. Agrega la información de cada sucursal que recibirá productos del obrador central.",
"info_box": {
"title": "Modelo de Negocio Enterprise",
"description": "Tu obrador central se encargará de la producción, y las sucursales recibirán los productos terminados mediante transferencias internas optimizadas."
},
"list": {
"title": "Sucursales",
"add_button": "Agregar Sucursal",
"empty_state": {
"title": "No hay sucursales agregadas",
"description": "Comienza agregando las sucursales que forman parte de tu red empresarial",
"button": "Agregar Primera Sucursal"
}
},
"modal": {
"title_add": "Agregar Sucursal",
"title_edit": "Editar Sucursal",
"fields": {
"name": "Nombre de la Sucursal",
"location_code": "Código de Ubicación",
"location_code_help": "Un código corto para identificar esta ubicación",
"location_code_max": "(máx. 10 caracteres)",
"business_type": "Tipo de Negocio",
"business_model": "Modelo de Negocio",
"phone": "Teléfono",
"email": "Email",
"timezone": "Zona Horaria",
"zone": "Zona / Barrio (opcional)",
"zone_help": "Zona o barrio específico dentro de la ciudad",
"address": "Dirección",
"city": "Ciudad",
"postal_code": "Código Postal"
},
"placeholders": {
"name": "ej. Madrid - Salamanca",
"location_code": "ej. MAD, BCN, VAL",
"phone": "ej. +34 123 456 789",
"email": "ej. contacto@panaderia.com",
"zone": "ej. Salamanca, Chamberí, Centro",
"address": "ej. Calle de Serrano, 48",
"city": "ej. Madrid",
"postal_code": "ej. 28001"
},
"business_types": {
"bakery": "Panadería",
"coffee_shop": "Cafetería",
"pastry_shop": "Pastelería",
"restaurant": "Restaurante"
},
"business_models": {
"retail_bakery": "Panadería Minorista",
"central_baker_satellite": "Obrador Central + Sucursales",
"hybrid_bakery": "Modelo Híbrido"
},
"timezones": {
"europe_madrid": "Europe/Madrid (UTC+1/UTC+2)",
"europe_paris": "Europe/Paris (UTC+1/UTC+2)",
"europe_london": "Europe/London (UTC+0/UTC+1)",
"america_new_york": "America/New_York (UTC-5/UTC-4)"
},
"buttons": {
"cancel": "Cancelar",
"save": "Guardar Cambios",
"add": "Agregar Sucursal"
}
},
"card": {
"edit": "Editar",
"delete": "Eliminar"
},
"continue_button": "Continuar con {count} {count, plural, one {Sucursal} other {Sucursales}}",
"validation": {
"name_required": "El nombre es requerido",
"city_required": "La ciudad es requerida",
"address_required": "La dirección es requerida",
"postal_code_required": "El código postal es requerido",
"postal_code_format": "El código postal debe tener exactamente 5 dígitos",
"location_code_required": "El código de ubicación es requerido",
"location_code_max": "El código no debe exceder 10 caracteres",
"location_code_format": "Solo se permiten letras mayúsculas, números y guiones/guiones bajos",
"phone_format": "Introduce un número de teléfono español válido",
"email_format": "Introduce un correo electrónico válido"
},
"alerts": {
"require_one": "Debes agregar al menos una sucursal para continuar"
}
} }
} }

View File

@@ -99,23 +99,63 @@
"steps": { "steps": {
"tenant_registration": { "tenant_registration": {
"title": "Zure Okindegiko Informazioa", "title": "Zure Okindegiko Informazioa",
"title_enterprise": "Zure Okinleku Zentraleko Informazioa",
"subtitle": "Kontaiguzu zure negozioari buruz", "subtitle": "Kontaiguzu zure negozioari buruz",
"header": {
"title": "Erregistratu Zure Okindegia",
"title_enterprise": "Erregistratu Zure Okinleku Zentrala",
"description": "Osatu zure okindegiko oinarrizko informazioa hasteko.",
"description_enterprise": "Sartu zure okinleku nagusiaren datuak. Gero adarrak gehitu ahal izango dituzu."
},
"fields": { "fields": {
"business_name": "Negozioaren izena", "business_name": "Okindegiko Izena",
"business_name_enterprise": "Okinleku Zentralaren Izena",
"business_type": "Negozio mota", "business_type": "Negozio mota",
"address": "Helbidea", "address": "Helbidea",
"phone": "Telefonoa", "phone": "Telefonoa",
"postal_code": "Posta Kodea",
"city": "Hiria (Aukerakoa)",
"email": "Harremaneko emaila", "email": "Harremaneko emaila",
"website": "Webgunea (aukerakoa)", "website": "Webgunea (aukerakoa)",
"description": "Negozioaren deskripzioa" "description": "Negozioaren deskripzioa"
}, },
"placeholders": { "placeholders": {
"business_name": "Adib.: San José Okindegia", "business_name": "Sartu zure okindegiko izena",
"address": "Kale Nagusia 123, Hiria", "business_name_enterprise": "Sartu zure okinleku zentralaren izena",
"address": "Zure okindegiko helbidea...",
"address_enterprise": "Okinleku zentralaren helbidea...",
"phone": "+34 123 456 789", "phone": "+34 123 456 789",
"postal_code": "28001",
"city": "Madrid",
"email": "kontaktua@okindegia.com", "email": "kontaktua@okindegia.com",
"website": "https://nireokindegia.com", "website": "https://nireokindegia.com",
"description": "Deskribatu zure okindegia..." "description": "Deskribatu zure okindegia..."
},
"validation": {
"name_required": "Okindegiko izena beharrezkoa da",
"name_length": "Izenak 2 eta 200 karaktere artean izan behar ditu",
"address_required": "Helbidea beharrezkoa da",
"address_length": "Helbideak 10 eta 500 karaktere artean izan behar ditu",
"postal_code_required": "Posta kodea beharrezkoa da",
"postal_code_format": "Posta kodeak 5 digitu izan behar ditu",
"phone_required": "Telefono zenbakia beharrezkoa da",
"phone_length": "Telefonoak 9 eta 20 karaktere artean izan behar ditu",
"phone_format": "Mesedez, sartu Espainiako telefono zenbaki baliozkoa"
},
"buttons": {
"create": "Sortu Okindegia eta Jarraitu →",
"create_enterprise": "Sortu Okinleku Zentrala eta Jarraitu →",
"update": "Eguneratu Okindegia eta Jarraitu →",
"update_enterprise": "Eguneratu Okinleku Zentrala eta Jarraitu →"
},
"loading": {
"creating": "Erregistratzen...",
"creating_enterprise": "Okinlekua erregistratzen...",
"updating": "Okinlekua eguneratzen..."
},
"errors": {
"register": "Errorea okindegia erregistratzean. Saiatu berriro mesedez.",
"register_title": "Erregistro Errorea"
} }
}, },
"inventory_setup": { "inventory_setup": {
@@ -189,6 +229,7 @@
"ml_training": { "ml_training": {
"title": "AA Prestakuntza", "title": "AA Prestakuntza",
"subtitle": "Zure modelo pertsonalizatua sortzen", "subtitle": "Zure modelo pertsonalizatua sortzen",
"intro_text": "Bikain! Orain automatikoki entrenatuko dugu zure adimen artifizialaren modeloa eman dituzun salmenta eta inbentario datuen bitartez. Prozesu honek minutu batzuk iraun ditzake.",
"status": { "status": {
"preparing": "Datuak prestatzen...", "preparing": "Datuak prestatzen...",
"training": "Modeloa entrenatzen...", "training": "Modeloa entrenatzen...",
@@ -199,11 +240,30 @@
"data_preparation": "Datuen prestaketa", "data_preparation": "Datuen prestaketa",
"model_training": "Modeloaren prestakuntza", "model_training": "Modeloaren prestakuntza",
"validation": "Balioespena", "validation": "Balioespena",
"deployment": "Hedapena" "deployment": "Hedapena",
"processing": "Prozesatzen..."
}, },
"estimated_time": "Aurreikusitako denbora: {{minutes}} minutu", "estimated_time": "Aurreikusitako denbora: {{minutes}} minutu",
"estimated_time_remaining": "Geratzen den denbora aurreikusia: {{time}}", "estimated_time_remaining": "Geratzen den denbora aurreikusia: {{time}}",
"description": "AA modelo pertsonalizatu bat sortzen ari gara zure okindegiarentzat zure datu historikoen oinarrian.", "description": "AA modelo pertsonalizatu bat sortzen ari gara zure okindegiarentzat zure datu historikoen oinarrian.",
"training_info": {
"title": "Zer gertatzen da prestakuntzaren bitartean?",
"step1": "Salmenta-eredu historikoen azterketa",
"step2": "Eskaera-aurreikuspen ereduen sorrera",
"step3": "Inbentario algoritmoen optimizazioa",
"step4": "Balioespena eta zehaztasun-doikuntza"
},
"messages": {
"training": "Modeloa entrenatzen...",
"completed": "Prestakuntza ongi osatu da",
"completed_http": "Prestakuntza ongi osatu da (HTTP egiaztapen bidez detektatu da)",
"starting": "Modeloaren prestakuntza hasten...",
"queued": "Prestakuntza lana ilaran...",
"preparing": "Prestakuntza datuak prestatzen...",
"live": "Zuzenean",
"reconnecting": "Birkonektatzen...",
"estimated_completion": "Amaituko da:"
},
"skip_to_dashboard": { "skip_to_dashboard": {
"title": "Denbora luzea hartzen al du?", "title": "Denbora luzea hartzen al du?",
"description": "Prestakuntza atzeko planoan jarraitzen du. Panelera joan zaitezke orain eta sistema arakatu modeloa entrenatzen amaitzen duen bitartean.", "description": "Prestakuntza atzeko planoan jarraitzen du. Panelera joan zaitezke orain eta sistema arakatu modeloa entrenatzen amaitzen duen bitartean.",
@@ -403,6 +463,95 @@
"no_products_title": "Hasierako Stocka", "no_products_title": "Hasierako Stocka",
"no_products_message": "Stock-mailak geroago konfigura ditzakezu inbentario atalean." "no_products_message": "Stock-mailak geroago konfigura ditzakezu inbentario atalean."
}, },
"child_tenants": {
"title": "Sukurtsalen Konfigurazioa",
"subtitle": "Enterprise mailako enpresa gisa, okinleku zentral bat eta hainbat sukurtsal dituzu. Gehitu okinleku zentraletik produktuak jasoko dituen sukurtsal bakoitzaren informazioa.",
"info_box": {
"title": "Enterprise Negozio Eredua",
"description": "Zure okinleku zentralak ekoizpena kudeatzen du, eta sukurtsalek produktu amaituak jasoko dituzte barne transferentzia optimizatuen bidez."
},
"list": {
"title": "Sukurtsalak",
"add_button": "Sukurtsala Gehitu",
"empty_state": {
"title": "Ez dago sukurtsalik gehituta",
"description": "Hasi zure enpresa-sareko parte diren sukurtsalak gehitzen",
"button": "Lehenengo Sukurtsala Gehitu"
}
},
"modal": {
"title_add": "Sukurtsala Gehitu",
"title_edit": "Sukurtsala Editatu",
"fields": {
"name": "Sukurtsalaren Izena",
"location_code": "Kokapen Kodea",
"location_code_help": "Kokapen hau identifikatzeko kode laburra",
"location_code_max": "(gehienez 10 karaktere)",
"business_type": "Negozio Mota",
"business_model": "Negozio Eredua",
"phone": "Telefonoa",
"email": "Emaila",
"timezone": "Ordu-zona",
"zone": "Eremua / Auzoa (aukerakoa)",
"zone_help": "Hiriaren barruko eremu edo auzo zehatza",
"address": "Helbidea",
"city": "Hiria",
"postal_code": "Posta Kodea"
},
"placeholders": {
"name": "adib. Madrid - Salamanca",
"location_code": "adib. MAD, BCN, VAL",
"phone": "adib. +34 123 456 789",
"email": "adib. kontaktua@okindegia.com",
"zone": "adib. Salamanca, Erdialdea, Zentroa",
"address": "adib. Serrano Kalea, 48",
"city": "adib. Madrid",
"postal_code": "adib. 28001"
},
"business_types": {
"bakery": "Okindegia",
"coffee_shop": "Kafetegia",
"pastry_shop": "Pasteldegia",
"restaurant": "Jatetxea"
},
"business_models": {
"retail_bakery": "Txikizkako Okindegia",
"central_baker_satellite": "Okinleku Zentrala + Sukurtsalak",
"hybrid_bakery": "Eredu Mistoa"
},
"timezones": {
"europe_madrid": "Europe/Madrid (UTC+1/UTC+2)",
"europe_paris": "Europe/Paris (UTC+1/UTC+2)",
"europe_london": "Europe/London (UTC+0/UTC+1)",
"america_new_york": "America/New_York (UTC-5/UTC-4)"
},
"buttons": {
"cancel": "Ezeztatu",
"save": "Aldaketak Gorde",
"add": "Sukurtsala Gehitu"
}
},
"card": {
"edit": "Editatu",
"delete": "Ezabatu"
},
"continue_button": "Jarraitu {count} {count, plural, one {Sukurtsalarekin} other {Sukurtsalekin}}",
"validation": {
"name_required": "Izena beharrezkoa da",
"city_required": "Hiria beharrezkoa da",
"address_required": "Helbidea beharrezkoa da",
"postal_code_required": "Posta kodea beharrezkoa da",
"postal_code_format": "Posta kodeak 5 digitu izan behar ditu",
"location_code_required": "Kokapen kodea beharrezkoa da",
"location_code_max": "Kodeak ezin du 10 karaktere baino gehiago izan",
"location_code_format": "Letra larri, zenbaki eta marratxoak/azpiko marratxoak bakarrik onartzen dira",
"phone_format": "Mesedez, sartu Espainiako telefono zenbaki baliozkoa",
"email_format": "Mesedez, sartu helbide elektroniko baliozkoa"
},
"alerts": {
"require_one": "Gutxienez sukurtsal bat gehitu behar duzu jarraitzeko"
}
},
"errors": { "errors": {
"step_failed": "Errorea pauso honetan", "step_failed": "Errorea pauso honetan",
"data_invalid": "Datu baliogabeak", "data_invalid": "Datu baliogabeak",