Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ META_APP_SECRET=your-meta-app-secret
# or a background worker that has no incoming request.
NEXT_PUBLIC_SITE_URL=https://crm.example.com

# Default language locale (e.g. en)
NEXT_PUBLIC_APP_LOCALE=en
# Default language locale (e.g. es)
NEXT_PUBLIC_APP_LOCALE=es

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please keep this on en. Changing the committed default makes every new install and every forker who hasn't set the var boot in Spanish — the language should be opt-in. Document es as an available locale instead.


# ============================================================
# OPTIONAL — only needed if you use the feature.
Expand Down
1,696 changes: 1,696 additions & 0 deletions messages/es.json

Large diffs are not rendered by default.

22 changes: 11 additions & 11 deletions src/app/(auth)/forgot-password/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,12 @@ export default function ForgotPasswordPage() {
<CheckCircle className="h-6 w-6 text-primary" />
</div>
<CardTitle className="text-xl text-foreground">
Check your email
Revisa tu correo
</CardTitle>
<CardDescription className="text-muted-foreground">
We&apos;ve sent a password reset link to{" "}
<span className="text-foreground">{email}</span>. Please check your
inbox.
Enviamos un enlace para restablecer tu contraseña a{" "}
<span className="text-foreground">{email}</span>. Revisa tu
bandeja de entrada.
</CardDescription>
</CardHeader>
<CardContent>
Expand All @@ -64,7 +64,7 @@ export default function ForgotPasswordPage() {
variant="outline"
className="w-full border-border text-muted-foreground hover:bg-muted hover:text-foreground"
>
Back to sign in
Volver a iniciar sesión
</Button>
</Link>
</CardContent>
Expand All @@ -80,9 +80,9 @@ export default function ForgotPasswordPage() {
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10">
<MessageSquare className="h-6 w-6 text-primary" />
</div>
<CardTitle className="text-xl text-foreground">Reset password</CardTitle>
<CardTitle className="text-xl text-foreground">Restablecer contraseña</CardTitle>
<CardDescription className="text-muted-foreground">
Enter your email and we&apos;ll send you a reset link
Ingresa tu correo y te enviaremos un enlace para restablecerla
</CardDescription>
</CardHeader>
<CardContent>
Expand All @@ -95,12 +95,12 @@ export default function ForgotPasswordPage() {

<div className="flex flex-col gap-2">
<Label htmlFor="email" className="text-muted-foreground">
Email
Correo electrónico
</Label>
<Input
id="email"
type="email"
placeholder="you@example.com"
placeholder="tu@ejemplo.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
Expand All @@ -113,7 +113,7 @@ export default function ForgotPasswordPage() {
disabled={loading}
className="mt-2 h-10 w-full bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
>
{loading ? "Sending..." : "Send reset link"}
{loading ? "Enviando..." : "Enviar enlace"}
</Button>
</form>

Expand All @@ -122,7 +122,7 @@ export default function ForgotPasswordPage() {
className="mt-6 flex items-center justify-center gap-2 text-sm text-muted-foreground hover:text-foreground"
>
<ArrowLeft className="h-4 w-4" />
Back to sign in
Volver a iniciar sesión
</Link>
</CardContent>
</Card>
Expand Down
42 changes: 21 additions & 21 deletions src/app/(auth)/signup/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@ function SignupPageInner() {
setError(null);

if (password !== confirmPassword) {
setError("Passwords do not match");
setError("Las contraseñas no coinciden");

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hardcodes Spanish and bypasses next-intl, so it shows even under the default/fallback en. Please route these through t() with keys in both en.json and es.json. Same pattern applies across the other component files in this PR.

return;
}

if (password.length < 6) {
setError("Password must be at least 6 characters");
setError("La contraseña debe tener al menos 6 caracteres");
return;
}

Expand Down Expand Up @@ -98,12 +98,12 @@ function SignupPageInner() {
<CheckCircle className="h-6 w-6 text-primary" />
</div>
<CardTitle className="text-xl text-foreground">
Check your email
Revisa tu correo
</CardTitle>
<CardDescription className="text-muted-foreground">
We&apos;ve sent a confirmation link to{" "}
<span className="text-foreground">{email}</span>. Please check your
inbox and click the link to verify your account.
Enviamos un enlace de confirmación a{" "}
<span className="text-foreground">{email}</span>. Revisa tu
bandeja de entrada y haz clic en el enlace para verificar tu cuenta.
</CardDescription>
</CardHeader>
<CardContent>
Expand All @@ -118,7 +118,7 @@ function SignupPageInner() {
variant="outline"
className="w-full border-border text-muted-foreground hover:bg-muted hover:text-foreground"
>
Back to sign in
Volver a iniciar sesión
</Button>
</Link>
</CardContent>
Expand All @@ -139,12 +139,12 @@ function SignupPageInner() {
)}
</div>
<CardTitle className="text-xl text-foreground">
{inviteToken ? "Create account & join" : "Create account"}
{inviteToken ? "Crear cuenta y unirte" : "Crear cuenta"}
</CardTitle>
<CardDescription className="text-muted-foreground">
{inviteToken
? "Verify your email, then accept the invitation to join your team."
: "Get started with CRM Template for WhatsApp"}
? "Verifica tu correo y luego acepta la invitación para unirte a tu equipo."
: "Comienza con la plantilla de CRM para WhatsApp"}
</CardDescription>
</CardHeader>
<CardContent>
Expand All @@ -157,12 +157,12 @@ function SignupPageInner() {

<div className="flex flex-col gap-2">
<Label htmlFor="fullName" className="text-muted-foreground">
Full name
Nombre completo
</Label>
<Input
id="fullName"
type="text"
placeholder="John Doe"
placeholder="Juan Pérez"
value={fullName}
onChange={(e) => setFullName(e.target.value)}
required
Expand All @@ -172,12 +172,12 @@ function SignupPageInner() {

<div className="flex flex-col gap-2">
<Label htmlFor="email" className="text-muted-foreground">
Email
Correo electrónico
</Label>
<Input
id="email"
type="email"
placeholder="you@example.com"
placeholder="tu@ejemplo.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
Expand All @@ -187,12 +187,12 @@ function SignupPageInner() {

<div className="flex flex-col gap-2">
<Label htmlFor="password" className="text-muted-foreground">
Password
Contraseña
</Label>
<Input
id="password"
type="password"
placeholder="At least 6 characters"
placeholder="Al menos 6 caracteres"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
Expand All @@ -202,12 +202,12 @@ function SignupPageInner() {

<div className="flex flex-col gap-2">
<Label htmlFor="confirmPassword" className="text-muted-foreground">
Confirm password
Confirmar contraseña
</Label>
<Input
id="confirmPassword"
type="password"
placeholder="Repeat your password"
placeholder="Repite tu contraseña"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
Expand All @@ -220,12 +220,12 @@ function SignupPageInner() {
disabled={loading}
className="mt-2 h-10 w-full bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
>
{loading ? "Creating account..." : "Create account"}
{loading ? "Creando cuenta..." : "Crear cuenta"}
</Button>
</form>

<p className="mt-6 text-center text-sm text-muted-foreground">
Already have an account?{" "}
¿Ya tienes una cuenta?{" "}
<Link
href={
inviteToken
Expand All @@ -234,7 +234,7 @@ function SignupPageInner() {
}
className="text-primary hover:text-primary/80"
>
Sign in
Iniciar sesión
</Link>
</p>
</CardContent>
Expand Down
10 changes: 5 additions & 5 deletions src/app/(dashboard)/agents/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@ export default function AgentsPage() {
<div className="flex items-center gap-2">
<Bot className="h-6 w-6 text-primary" />
<h1 className="text-2xl font-bold tracking-tight text-foreground">
AI Agents
Agentes IA
</h1>
</div>
<p className="mt-1 text-sm text-muted-foreground">
Your bring-your-own-key AI agent — set it up, then test it in the
playground before it replies to customers in the inbox.
Tu agente de IA con clave propia — configúralo y luego pruébalo en el
playground antes de que responda a los clientes en la bandeja de entrada.
</p>

{decided && (
Expand All @@ -60,11 +60,11 @@ export default function AgentsPage() {
<Sparkles className="mr-1.5 h-4 w-4" /> Playground
</TabsTrigger>
<TabsTrigger value="setup">
<Settings2 className="mr-1.5 h-4 w-4" /> Setup
<Settings2 className="mr-1.5 h-4 w-4" /> Configuración
</TabsTrigger>
{canViewUsage && (
<TabsTrigger value="usage">
<BarChart3 className="mr-1.5 h-4 w-4" /> Usage
<BarChart3 className="mr-1.5 h-4 w-4" /> Uso
</TabsTrigger>
)}
</TabsList>
Expand Down
8 changes: 4 additions & 4 deletions src/app/(dashboard)/automations/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export default function AutomationsPage() {
if (fetchErr) throw fetchErr
setAutomations((data ?? []) as Automation[])
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load automations")
setError(err instanceof Error ? err.message : "No se pudieron cargar las automatizaciones")
}
}

Expand Down Expand Up @@ -169,7 +169,7 @@ export default function AutomationsPage() {
</div>
<GatedButton
canAct={canCreate}
gateReason="create automations"
gateReason="crear automatizaciones"
onClick={() => router.push("/automations/new")}
className="bg-primary text-primary-foreground hover:bg-primary/90"
>
Expand Down Expand Up @@ -299,7 +299,7 @@ function AutomationCard({
{automation.name}
</span>
{automation.is_active && (
<span className="relative flex h-2 w-2" aria-label="active">
<span className="relative flex h-2 w-2" aria-label="activo">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-primary opacity-75" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-primary" />
</span>
Expand Down Expand Up @@ -336,7 +336,7 @@ function AutomationCard({

<DropdownMenu>
<DropdownMenuTrigger
aria-label="Open menu"
aria-label="Abrir menú"
className="inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground data-[popup-open]:bg-muted"
>
<MoreVertical className="h-4 w-4" />
Expand Down
4 changes: 2 additions & 2 deletions src/app/(dashboard)/broadcasts/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ function FunnelChart({ steps }: { steps: FunnelStep[] }) {
const max = Math.max(...steps.map((s) => s.value), 1);
return (
<div className="rounded-xl border border-border bg-card p-4">
<h3 className="mb-4 text-sm font-medium text-foreground">Funnel</h3>
<h3 className="mb-4 text-sm font-medium text-foreground">Embudo</h3>
<div className="space-y-2">
{steps.map((step) => {
const pctOfMax = Math.max(5, Math.round((step.value / max) * 100));
Expand Down Expand Up @@ -487,7 +487,7 @@ export default function BroadcastDetailPage() {
return (
<TableRow key={recipient.id} className="border-border">
<TableCell className="font-medium text-foreground">
{recipient.contact?.name ?? 'Unknown'}
{recipient.contact?.name ?? 'Desconocido'}
</TableCell>
<TableCell className="text-muted-foreground">
{recipient.contact?.phone ?? '-'}
Expand Down
2 changes: 1 addition & 1 deletion src/app/(dashboard)/broadcasts/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export default function NewBroadcastPage() {
} catch (err) {
// Previously swallowed with console.error — the wizard would
// just no-op, leaving the user confused. Surface the reason.
const message = err instanceof Error ? err.message : 'Broadcast failed';
const message = err instanceof Error ? err.message : 'Error al enviar la difusión';
console.error('Broadcast failed:', err);
toast.error(message);
}
Expand Down
6 changes: 3 additions & 3 deletions src/app/(dashboard)/broadcasts/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export default function BroadcastsPage() {
{anySending && (
<div
role="progressbar"
aria-label="Broadcast in progress"
aria-label="Difusión en curso"
className="broadcast-indeterminate fixed inset-x-0 top-0 z-40 h-0.5 overflow-hidden bg-muted"
>
<div className="broadcast-indeterminate-bar h-0.5 bg-primary" />
Expand Down Expand Up @@ -189,7 +189,7 @@ export default function BroadcastsPage() {
</div>
<GatedButton
canAct={canCreate}
gateReason="create broadcasts"
gateReason="crear difusiones"
onClick={() => router.push('/broadcasts/new')}
className="bg-primary text-primary-foreground hover:bg-primary/90"
>
Expand All @@ -207,7 +207,7 @@ export default function BroadcastsPage() {
</p>
<GatedButton
canAct={canCreate}
gateReason="create broadcasts"
gateReason="crear difusiones"
onClick={() => router.push('/broadcasts/new')}
className="mt-4 bg-primary text-primary-foreground hover:bg-primary/90"
>
Expand Down
Loading