diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 896b429..e211d72 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -10,8 +10,8 @@ import "./globals.css" import { Suspense } from "react" export const metadata: Metadata = { - title: "GoHorseJobs - Find your next opportunity", - description: "Connecting candidates and companies quickly and directly", + title: "GoHorse Jobs - Encontre sua próxima oportunidade", + description: "Conectamos candidatos e empresas de forma rápida e direta", generator: "v0.app", icons: { icon: "/logohorse.png", @@ -26,7 +26,7 @@ export default function RootLayout({ children: React.ReactNode }>) { return ( - + diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 23a5a88..be931f6 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -34,10 +34,10 @@ export default function HomePage() { const mappedJobs: Job[] = data.data.map((j: any) => ({ id: String(j.id), title: j.title, - company: j.companyName || 'Empresa Confidencial', - location: j.location || 'Remoto', + company: j.companyName || t('jobs.confidential'), + location: j.location || t('workMode.remote'), type: j.employmentType || 'full-time', - salary: j.salaryMin ? `R$ ${j.salaryMin}` : 'A combinar', + salary: j.salaryMin ? `R$ ${j.salaryMin}` : t('jobs.salary.negotiable'), description: j.description, requirements: j.requirements || [], postedAt: j.createdAt, @@ -249,7 +249,7 @@ export default function HomePage() { {/* CTA Section */} -
+
diff --git a/frontend/src/app/vagas/page.tsx b/frontend/src/app/vagas/page.tsx index 61bab75..d41e853 100644 --- a/frontend/src/app/vagas/page.tsx +++ b/frontend/src/app/vagas/page.tsx @@ -15,11 +15,13 @@ import { PageSkeleton } from "@/components/loading-skeletons" import { mockJobs } from "@/lib/mock-data" import { jobsApi, transformApiJobToFrontend } from "@/lib/api" import { useDebounce } from "@/hooks/use-utils" +import { useTranslation } from "@/lib/i18n" import { Search, MapPin, Briefcase, SlidersHorizontal, X, ArrowUpDown } from "lucide-react" import { motion, AnimatePresence } from "framer-motion" import type { Job } from "@/lib/types" function JobsContent() { + const { t } = useTranslation() const [jobs, setJobs] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) @@ -69,7 +71,7 @@ function JobsContent() { } catch (err) { console.error("Erro ao buscar vagas", err) if (isMounted) { - setError("Não foi possível carregar as vagas agora. Exibindo exemplos.") + setError(t('jobs.error')) setJobs(mockJobs) } } finally { @@ -171,7 +173,7 @@ function JobsContent() { animate={{ opacity: 1, y: 0 }} className="text-4xl md:text-5xl font-bold text-foreground mb-4 text-balance" > - Encontre sua próxima oportunidade + {t('jobs.title')} - {loading ? "Carregando vagas..." : `${jobs.length} vagas disponíveis nas melhores empresas`} + {loading ? t('jobs.loading') : t('jobs.subtitle', { count: jobs.length })}
@@ -194,7 +196,7 @@ function JobsContent() {
setSearchTerm(e.target.value)} className="pl-10 h-12" @@ -208,7 +210,7 @@ function JobsContent() { className="h-12 gap-2" > - Filtros + {t('jobs.filters.toggle')} {hasActiveFilters && ( ! @@ -233,10 +235,10 @@ function JobsContent() { - + - Todos os tipos + {t('jobs.filters.all')} {uniqueTypes.map((type) => ( {type === "full-time" ? "Tempo integral" : @@ -265,15 +267,15 @@ function JobsContent() { - + - Mais recentes - Título - Empresa - Localização + {t('jobs.sort.recent')} + {t('jobs.sort.title')} + {t('jobs.sort.company')} + {t('jobs.sort.location')} @@ -299,7 +301,7 @@ function JobsContent() { className="gap-2" > - Limpar + {t('jobs.reset')} )}
@@ -312,8 +314,11 @@ function JobsContent() { {/* Results Summary */}
- {filteredAndSortedJobs.length} vaga{filteredAndSortedJobs.length !== 1 ? 's' : ''} encontrada{filteredAndSortedJobs.length !== 1 ? 's' : ''} - {totalPages > 1 && ` (Página ${currentPage} de ${totalPages})`} + {t('jobs.pagination.showing', { + from: (currentPage - 1) * ITEMS_PER_PAGE + 1, + to: Math.min(currentPage * ITEMS_PER_PAGE, filteredAndSortedJobs.length), + total: filteredAndSortedJobs.length + })} {hasActiveFilters && (
@@ -370,7 +375,7 @@ function JobsContent() { )} {loading ? ( -
Carregando vagas...
+
{t('jobs.loading')}
) : paginatedJobs.length > 0 ? (
@@ -398,17 +403,17 @@ function JobsContent() { onClick={() => setCurrentPage(p => Math.max(1, p - 1))} disabled={currentPage === 1} > - Anterior + {t('jobs.pagination.previous')}
- Página {currentPage} de {totalPages} + {currentPage} / {totalPages}
)} @@ -421,9 +426,9 @@ function JobsContent() { >
-

Nenhuma vaga encontrada

+

{t('jobs.noResults.title')}

- Não encontramos vagas que correspondam aos seus critérios de busca. + {t('jobs.noResults.desc')}

diff --git a/frontend/src/components/footer.tsx b/frontend/src/components/footer.tsx index 0541e58..99c77c9 100644 --- a/frontend/src/components/footer.tsx +++ b/frontend/src/components/footer.tsx @@ -1,6 +1,10 @@ import Link from "next/link" +import { useTranslation } from "@/lib/i18n" export function Footer() { + const { t } = useTranslation() + const currentYear = new Date().getFullYear() + return (
@@ -10,12 +14,12 @@ export function Footer() { GoHorse Jobs

- Conectamos candidatos e empresas de forma rápida e direta. Encontre sua próxima oportunidade profissional em tecnologia. + {t('home.hero.subtitle')}

-

Vagas por Tecnologia

+

{t('footer.jobsByTech')}

  • @@ -34,44 +38,44 @@ export function Footer() {
  • - Vagas Remotas + {t('workMode.remote')}
-

Empresa

+

{t('footer.company')}

  • - Sobre + {t('footer.about')}
  • - Contato + {t('nav.contact')}
  • - Todas as Vagas + {t('nav.jobs')}
-

Legal

+

{t('footer.legal')}

  • - Política de Privacidade + {t('footer.privacy')}
  • - Termos de Uso + {t('footer.terms')}
@@ -79,7 +83,7 @@ export function Footer() {
-

© 2025 GoHorse Jobs. Todos os direitos reservados.

+

{t('footer.copyright', { year: currentYear })}

diff --git a/frontend/src/components/job-card.tsx b/frontend/src/components/job-card.tsx index 6f0ee6b..ff7c1cb 100644 --- a/frontend/src/components/job-card.tsx +++ b/frontend/src/components/job-card.tsx @@ -20,12 +20,14 @@ import Link from "next/link"; import { motion } from "framer-motion"; import { useState } from "react"; import { useNotify } from "@/contexts/notification-context"; +import { useTranslation } from "@/lib/i18n"; interface JobCardProps { job: Job; } export function JobCard({ job }: JobCardProps) { + const { t } = useTranslation(); const [isFavorited, setIsFavorited] = useState(false); const notify = useNotify(); @@ -35,21 +37,15 @@ export function JobCard({ job }: JobCardProps) { const diffInMs = now.getTime() - date.getTime(); const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24)); - if (diffInDays === 0) return "Hoje"; - if (diffInDays === 1) return "Ontem"; - if (diffInDays < 7) return `${diffInDays} dias atrás`; - if (diffInDays < 30) return `${Math.floor(diffInDays / 7)} semanas atrás`; - return `${Math.floor(diffInDays / 30)} meses atrás`; + if (diffInDays === 0) return t('jobs.posted.today'); + if (diffInDays === 1) return t('jobs.posted.yesterday'); + if (diffInDays < 7) return t('jobs.posted.daysAgo', { count: diffInDays }); + if (diffInDays < 30) return t('jobs.posted.weeksAgo', { count: Math.floor(diffInDays / 7) }); + return t('jobs.posted.monthsAgo', { count: Math.floor(diffInDays / 30) }); }; const getTypeLabel = (type: string) => { - const typeLabels: { [key: string]: string } = { - "full-time": "Tempo integral", - "part-time": "Meio período", - contract: "Contrato", - Remoto: "Remoto", - }; - return typeLabels[type] || type; + return t(`jobs.types.${type}`) !== `jobs.types.${type}` ? t(`jobs.types.${type}`) : type; }; const getTypeBadgeVariant = (type: string) => { @@ -80,11 +76,11 @@ export function JobCard({ job }: JobCardProps) { setIsFavorited(!isFavorited); if (!isFavorited) { notify.info( - "Vaga favoritada!", - `${job.title} foi adicionada aos seus favoritos.`, + t('jobs.favorites.added.title'), + t('jobs.favorites.added.desc', { title: job.title }), { actionUrl: "/dashboard/candidato/favoritos", - actionLabel: "Ver favoritos", + actionLabel: t('jobs.favorites.action'), } ); } @@ -183,7 +179,7 @@ export function JobCard({ job }: JobCardProps) { variant="outline" className="text-xs text-muted-foreground" > - +{job.requirements.length - 3} mais + {t('jobs.requirements.more', { count: job.requirements.length - 3 })} )} @@ -200,11 +196,11 @@ export function JobCard({ job }: JobCardProps) {
- +
diff --git a/frontend/src/components/navbar.tsx b/frontend/src/components/navbar.tsx index 8ea547c..f926c10 100644 --- a/frontend/src/components/navbar.tsx +++ b/frontend/src/components/navbar.tsx @@ -114,13 +114,13 @@ export function Navbar() { setIsOpen(false)}> setIsOpen(false)}> diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index a3c2689..d904acc 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -39,8 +39,20 @@ "title": "Find your next opportunity", "subtitle": "{count} jobs available at the best companies", "search": "Search jobs by title, company...", - "filters": { "all": "All", "onsite": "On-site", "hybrid": "Hybrid", "remote": "Remote", "workMode": "Work Mode" }, + "filters": { "all": "All", "toggle": "Filters", "location": "Location", "type": "Type", "workMode": "Work Mode", "order": "Sort by" }, + "sort": { "recent": "Most recent", "title": "Title", "company": "Company", "location": "Location" }, + "reset": "Clear", + "resetFilters": "Clear filters", + "noResults": { "title": "No jobs found", "desc": "We couldn't find any jobs matching your criteria." }, + "loading": "Loading jobs...", + "error": "Could not load jobs right now. Showing examples.", "card": { "viewDetails": "View details", "apply": "Apply now", "perMonth": "/month", "postedAgo": "Posted {time} ago" }, + "types": { "full-time": "Full Time", "part-time": "Part Time", "contract": "Contract", "freelance": "Freelance" }, + "confidential": "Confidential Company", + "salary": { "negotiable": "Negotiable" }, + "posted": { "today": "Today", "yesterday": "Yesterday", "daysAgo": "{count} days ago", "weeksAgo": "{count} weeks ago", "monthsAgo": "{count} months ago" }, + "favorites": { "added": { "title": "Job favorited!", "desc": "{title} added to your favorites." }, "action": "View favorites" }, + "requirements": { "more": "+{count} more" }, "pagination": { "previous": "Previous", "next": "Next", "showing": "Showing {from} to {to} of {total} jobs" } }, "workMode": { "onsite": "On-site", "hybrid": "Hybrid", "remote": "Remote" }, diff --git a/frontend/src/i18n/pt-BR.json b/frontend/src/i18n/pt-BR.json index 3702a38..d541895 100644 --- a/frontend/src/i18n/pt-BR.json +++ b/frontend/src/i18n/pt-BR.json @@ -39,8 +39,20 @@ "title": "Encontre sua próxima oportunidade", "subtitle": "{count} vagas disponíveis nas melhores empresas", "search": "Buscar vagas por título, empresa...", - "filters": { "all": "Todas", "onsite": "Presencial", "hybrid": "Híbrido", "remote": "Remoto", "workMode": "Modalidade" }, + "filters": { "all": "Todas", "toggle": "Filtros", "location": "Localização", "type": "Tipo", "workMode": "Modalidade", "order": "Ordenar por" }, + "sort": { "recent": "Mais recentes", "title": "Título", "company": "Empresa", "location": "Localização" }, + "reset": "Limpar", + "resetFilters": "Limpar filtros", + "noResults": { "title": "Nenhuma vaga encontrada", "desc": "Não encontramos vagas que correspondam aos seus critérios de busca." }, + "loading": "Carregando vagas...", + "error": "Não foi possível carregar as vagas agora. Exibindo exemplos.", "card": { "viewDetails": "Ver detalhes", "apply": "Candidatar-se", "perMonth": "/mês", "postedAgo": "Publicada há {time}" }, + "types": { "full-time": "Tempo Integral", "part-time": "Meio Período", "contract": "Contrato", "freelance": "Freelance" }, + "confidential": "Empresa Confidencial", + "salary": { "negotiable": "A combinar" }, + "posted": { "today": "Hoje", "yesterday": "Ontem", "daysAgo": "{count} dias atrás", "weeksAgo": "{count} semanas atrás", "monthsAgo": "{count} meses atrás" }, + "favorites": { "added": { "title": "Vaga favoritada!", "desc": "{title} foi adicionada aos seus favoritos." }, "action": "Ver favoritos" }, + "requirements": { "more": "+{count} mais" }, "pagination": { "previous": "Anterior", "next": "Próximo", "showing": "Mostrando {from} a {to} de {total} vagas" } }, "workMode": { "onsite": "Presencial", "hybrid": "Híbrido", "remote": "Remoto" }, diff --git a/frontend/src/lib/i18n.tsx b/frontend/src/lib/i18n.tsx index c9a3d5e..e65a80c 100644 --- a/frontend/src/lib/i18n.tsx +++ b/frontend/src/lib/i18n.tsx @@ -22,7 +22,7 @@ const dictionaries: Record = { const I18nContext = createContext(null); export function I18nProvider({ children }: { children: ReactNode }) { - const [locale, setLocale] = useState('en'); + const [locale, setLocale] = useState('pt-BR'); const t = useCallback((key: string, params?: Record): string => { const keys = key.split('.');