From ce26de622536ccac76209fb164b61c5568fa9dec Mon Sep 17 00:00:00 2001 From: Tiago Yamamoto Date: Sat, 3 Jan 2026 19:57:09 -0300 Subject: [PATCH] Fix locale sync for post job and candidates --- .../src/app/dashboard/candidates/page.tsx | 58 ++++++++++--------- frontend/src/app/post-job/page.test.tsx | 14 ++++- frontend/src/app/post-job/page.tsx | 33 ++++++----- frontend/src/app/post-job/translations.ts | 30 ++++++++++ frontend/src/i18n/en.json | 41 +++++++++++++ frontend/src/i18n/es.json | 41 +++++++++++++ frontend/src/i18n/pt-BR.json | 41 +++++++++++++ 7 files changed, 216 insertions(+), 42 deletions(-) diff --git a/frontend/src/app/dashboard/candidates/page.tsx b/frontend/src/app/dashboard/candidates/page.tsx index 9fd5c73..3df8956 100644 --- a/frontend/src/app/dashboard/candidates/page.tsx +++ b/frontend/src/app/dashboard/candidates/page.tsx @@ -17,8 +17,10 @@ import { } from "@/components/ui/dialog" import { Search, Eye, Mail, Phone, MapPin, Briefcase } from "lucide-react" import { adminCandidatesApi, AdminCandidate, AdminCandidateStats } from "@/lib/api" +import { useTranslation } from "@/lib/i18n" export default function AdminCandidatesPage() { + const { t } = useTranslation() const [searchTerm, setSearchTerm] = useState("") const [selectedCandidate, setSelectedCandidate] = useState(null) const [candidates, setCandidates] = useState([]) @@ -39,7 +41,7 @@ export default function AdminCandidatesPage() { setStats(response.stats) } catch (error) { if (!isMounted) return - setErrorMessage(error instanceof Error ? error.message : "Failed to load candidates") + setErrorMessage(error instanceof Error ? error.message : t("admin.candidates_page.load_error")) } finally { if (isMounted) { setIsLoading(false) @@ -68,33 +70,33 @@ export default function AdminCandidatesPage() {
{/* Header */}
-

Candidate management

-

View and manage all registered candidates

+

{t("admin.candidates_page.title")}

+

{t("admin.candidates_page.subtitle")}

{/* Stats */}
- Total candidates + {t("admin.candidates_page.stats.total")} {stats?.totalCandidates ?? 0} - New (30 days) + {t("admin.candidates_page.stats.new")} {stats?.newCandidates ?? 0} - Active applications + {t("admin.candidates_page.stats.active")} {stats?.activeApplications ?? 0} - Hiring rate + {t("admin.candidates_page.stats.hiring_rate")} {Math.round(stats?.hiringRate ?? 0)}% @@ -107,7 +109,7 @@ export default function AdminCandidatesPage() {
setSearchTerm(e.target.value)} className="pl-10" @@ -122,26 +124,26 @@ export default function AdminCandidatesPage() { - Candidate - Email - Phone - Location - Applications - Actions + {t("admin.candidates_page.table.candidate")} + {t("admin.candidates_page.table.email")} + {t("admin.candidates_page.table.phone")} + {t("admin.candidates_page.table.location")} + {t("admin.candidates_page.table.applications")} + {t("admin.candidates_page.table.actions")} {isLoading && ( - Loading candidates... + {t("admin.candidates_page.table.loading")} )} {!isLoading && filteredCandidates.length === 0 && ( - No candidates found. + {t("admin.candidates_page.table.empty")} )} @@ -170,8 +172,10 @@ export default function AdminCandidatesPage() { - Candidate profile - Detailed information about {candidate.name} + {t("admin.candidates_page.dialog.title")} + + {t("admin.candidates_page.dialog.description", { name: candidate.name })} + {selectedCandidate && (
@@ -220,18 +224,18 @@ export default function AdminCandidatesPage() {
-

About

+

{t("admin.candidates_page.about.title")}

- {selectedCandidate.bio ?? "No profile summary provided."} + {selectedCandidate.bio ?? t("admin.candidates_page.about.empty")}

-

Recent applications

+

{t("admin.candidates_page.applications.title")}

{selectedCandidate.applications.length === 0 && (
- No applications submitted yet. + {t("admin.candidates_page.applications.empty")}
)} {selectedCandidate.applications.map((app) => ( @@ -252,11 +256,11 @@ export default function AdminCandidatesPage() { : "secondary" } > - {app.status === "pending" && "Pending"} - {app.status === "reviewed" && "Reviewed"} - {app.status === "shortlisted" && "Shortlisted"} - {app.status === "hired" && "Hired"} - {app.status === "rejected" && "Rejected"} + {app.status === "pending" && t("admin.candidates_page.status.pending")} + {app.status === "reviewed" && t("admin.candidates_page.status.reviewed")} + {app.status === "shortlisted" && t("admin.candidates_page.status.shortlisted")} + {app.status === "hired" && t("admin.candidates_page.status.hired")} + {app.status === "rejected" && t("admin.candidates_page.status.rejected")}
))} diff --git a/frontend/src/app/post-job/page.test.tsx b/frontend/src/app/post-job/page.test.tsx index a156d31..7f0c46f 100644 --- a/frontend/src/app/post-job/page.test.tsx +++ b/frontend/src/app/post-job/page.test.tsx @@ -31,8 +31,8 @@ jest.mock("sonner", () => ({ jest.mock("./translations", () => ({ translations: { - pt: { - title: "Postar uma Vaga", + en: { + title: "Post a Job", buttons: { next: "Next Step", publish: "Publish Job", publishing: "Publishing..." }, company: { name: "Company Name", @@ -59,6 +59,16 @@ jest.mock("./translations", () => ({ contract: { permanent: "Permanent" }, hours: { fullTime: "Full Time" }, mode: { remote: "Remote" } + }, + errors: { + company_required: "Company required", + password_mismatch: "Password mismatch", + password_length: "Password length", + job_required: "Job required", + submit_failed: "Submit failed" + }, + success: { + job_created: "Job created" } }, }, diff --git a/frontend/src/app/post-job/page.tsx b/frontend/src/app/post-job/page.tsx index 2c22d5c..0fc8515 100644 --- a/frontend/src/app/post-job/page.tsx +++ b/frontend/src/app/post-job/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { useRouter } from "next/navigation"; import { translations, Language } from "./translations"; import { toast } from "sonner"; @@ -18,6 +18,7 @@ import { import { LocationPicker } from "@/components/location-picker"; import { RichTextEditor } from "@/components/rich-text-editor"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { useTranslation } from "@/lib/i18n"; // Common Country Codes const COUNTRY_CODES = [ @@ -53,9 +54,9 @@ const getCurrencySymbol = (code: string): string => { export default function PostJobPage() { const router = useRouter(); const [step, setStep] = useState<1 | 2>(1); + const { locale, setLocale } = useTranslation(); - // Language State - const [lang, setLang] = useState('pt'); + const lang = useMemo(() => (locale === "pt-BR" ? "pt" : locale), [locale]); const t = translations[lang]; // Helper inside to use t @@ -116,25 +117,25 @@ export default function PostJobPage() { const validateForm = () => { if (!company.name || !company.email || !company.password) { - toast.error("Preencha os dados obrigatórios da empresa"); + toast.error(t.errors.company_required); setStep(1); // Ensure we are on step 1 for company data errors return false; } if (company.password !== company.confirmPassword) { - toast.error("As senhas não coincidem"); + toast.error(t.errors.password_mismatch); setStep(1); // Ensure we are on step 1 for password mismatch return false; } if (company.password.length < 8) { - toast.error("A senha deve ter pelo menos 8 caracteres"); + toast.error(t.errors.password_length); setStep(1); // Ensure we are on step 1 for password length return false; } if (!job.title || !job.description) { - toast.error("Preencha os dados da vaga"); + toast.error(t.errors.job_required); setStep(1); // Stay on step 1 for job data errors return false; } @@ -144,15 +145,15 @@ export default function PostJobPage() { const handleNext = () => { // Only validate step 1 fields to move to step 2 if (!company.name || !company.email || !company.password) { - toast.error("Preencha os dados obrigatórios da empresa"); + toast.error(t.errors.company_required); return; } if (company.password !== company.confirmPassword) { - toast.error("As senhas não coincidem"); + toast.error(t.errors.password_mismatch); return; } if (company.password.length < 8) { - toast.error("A senha deve ter pelo menos 8 caracteres"); + toast.error(t.errors.password_length); return; } setStep(2); @@ -225,11 +226,11 @@ export default function PostJobPage() { localStorage.setItem("token", token); localStorage.setItem("auth_token", token); - toast.success("Vaga criada com sucesso! Aguardando aprovação."); + toast.success(t.success.job_created); router.push("/dashboard/jobs"); } catch (err: any) { - toast.error(err.message || "Erro ao processar solicitação"); + toast.error(err.message || t.errors.submit_failed); } finally { setLoading(false); } @@ -243,7 +244,13 @@ export default function PostJobPage() {
- { + const nextLang = value as Language; + setLocale(nextLang === "pt" ? "pt-BR" : nextLang); + }} + > diff --git a/frontend/src/app/post-job/translations.ts b/frontend/src/app/post-job/translations.ts index 0ee2b6f..fbe7370 100644 --- a/frontend/src/app/post-job/translations.ts +++ b/frontend/src/app/post-job/translations.ts @@ -87,6 +87,16 @@ export const translations = { next: "Próximo: Confirmar", publish: "Publicar Vaga", publishing: "Publicando..." + }, + errors: { + company_required: "Preencha os dados obrigatórios da empresa", + password_mismatch: "As senhas não coincidem", + password_length: "A senha deve ter pelo menos 8 caracteres", + job_required: "Preencha os dados da vaga", + submit_failed: "Erro ao processar solicitação" + }, + success: { + job_created: "Vaga criada com sucesso! Aguardando aprovação." } }, en: { @@ -177,6 +187,16 @@ export const translations = { next: "Next: Confirm", publish: "Publish Job", publishing: "Publishing..." + }, + errors: { + company_required: "Please fill in the required company details", + password_mismatch: "Passwords do not match", + password_length: "Password must be at least 8 characters", + job_required: "Please fill in the job details", + submit_failed: "Error while processing the request" + }, + success: { + job_created: "Job created successfully! Awaiting approval." } }, es: { @@ -267,6 +287,16 @@ export const translations = { next: "Siguiente: Confirmar", publish: "Publicar Empleo", publishing: "Publicando..." + }, + errors: { + company_required: "Complete los datos obligatorios de la empresa", + password_mismatch: "Las contraseñas no coinciden", + password_length: "La contraseña debe tener al menos 8 caracteres", + job_required: "Complete los datos del empleo", + submit_failed: "Error al procesar la solicitud" + }, + success: { + job_created: "Empleo creado correctamente. En espera de aprobación." } } } diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index 05f7c25..98bf27a 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -771,6 +771,47 @@ "delete_error": "Failed to delete user", "load_error": "Failed to load users" } + }, + "candidates_page": { + "title": "Candidate management", + "subtitle": "View and manage all registered candidates", + "load_error": "Failed to load candidates", + "stats": { + "total": "Total candidates", + "new": "New (30 days)", + "active": "Active applications", + "hiring_rate": "Hiring rate" + }, + "search_placeholder": "Search candidates by name or email...", + "table": { + "candidate": "Candidate", + "email": "Email", + "phone": "Phone", + "location": "Location", + "applications": "Applications", + "actions": "Actions", + "loading": "Loading candidates...", + "empty": "No candidates found." + }, + "dialog": { + "title": "Candidate profile", + "description": "Detailed information about {name}" + }, + "about": { + "title": "About", + "empty": "No profile summary provided." + }, + "applications": { + "title": "Recent applications", + "empty": "No applications submitted yet." + }, + "status": { + "pending": "Pending", + "reviewed": "Reviewed", + "shortlisted": "Shortlisted", + "hired": "Hired", + "rejected": "Rejected" + } } }, "company": { diff --git a/frontend/src/i18n/es.json b/frontend/src/i18n/es.json index 11b6d1a..e0724cc 100644 --- a/frontend/src/i18n/es.json +++ b/frontend/src/i18n/es.json @@ -772,6 +772,47 @@ "delete_error": "Error al eliminar usuario", "load_error": "Error al cargar usuarios" } + }, + "candidates_page": { + "title": "Gestión de candidatos", + "subtitle": "Ver y administrar todos los candidatos registrados", + "load_error": "Error al cargar candidatos", + "stats": { + "total": "Total de candidatos", + "new": "Nuevos (30 días)", + "active": "Postulaciones activas", + "hiring_rate": "Tasa de contratación" + }, + "search_placeholder": "Buscar candidatos por nombre o correo...", + "table": { + "candidate": "Candidato", + "email": "Correo", + "phone": "Teléfono", + "location": "Ubicación", + "applications": "Postulaciones", + "actions": "Acciones", + "loading": "Cargando candidatos...", + "empty": "No se encontraron candidatos." + }, + "dialog": { + "title": "Perfil del candidato", + "description": "Información detallada de {name}" + }, + "about": { + "title": "Acerca de", + "empty": "No se proporcionó un resumen del perfil." + }, + "applications": { + "title": "Postulaciones recientes", + "empty": "Aún no hay postulaciones." + }, + "status": { + "pending": "Pendiente", + "reviewed": "Revisado", + "shortlisted": "Preseleccionado", + "hired": "Contratado", + "rejected": "Rechazado" + } } }, "company": { diff --git a/frontend/src/i18n/pt-BR.json b/frontend/src/i18n/pt-BR.json index d2670ec..2f851dd 100644 --- a/frontend/src/i18n/pt-BR.json +++ b/frontend/src/i18n/pt-BR.json @@ -771,6 +771,47 @@ "delete_error": "Falha ao excluir usuário", "load_error": "Falha ao carregar usuários" } + }, + "candidates_page": { + "title": "Gestão de candidatos", + "subtitle": "Veja e gerencie todos os candidatos cadastrados", + "load_error": "Falha ao carregar candidatos", + "stats": { + "total": "Total de candidatos", + "new": "Novos (30 dias)", + "active": "Candidaturas ativas", + "hiring_rate": "Taxa de contratação" + }, + "search_placeholder": "Buscar candidatos por nome ou e-mail...", + "table": { + "candidate": "Candidato", + "email": "E-mail", + "phone": "Telefone", + "location": "Localização", + "applications": "Candidaturas", + "actions": "Ações", + "loading": "Carregando candidatos...", + "empty": "Nenhum candidato encontrado." + }, + "dialog": { + "title": "Perfil do candidato", + "description": "Informações detalhadas sobre {name}" + }, + "about": { + "title": "Sobre", + "empty": "Nenhum resumo do perfil foi fornecido." + }, + "applications": { + "title": "Candidaturas recentes", + "empty": "Nenhuma candidatura enviada ainda." + }, + "status": { + "pending": "Pendente", + "reviewed": "Em análise", + "shortlisted": "Pré-selecionado", + "hired": "Contratado", + "rejected": "Rejeitado" + } } }, "company": {