feat(frontend): complete i18n implementation and set default to pt-BR
This commit is contained in:
parent
dd18c526e3
commit
4693bc5737
9 changed files with 99 additions and 70 deletions
|
|
@ -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 (
|
||||
<html lang="en">
|
||||
<html lang="pt-BR">
|
||||
<body className={`font-sans ${GeistSans.variable} ${GeistMono.variable} antialiased`}>
|
||||
<I18nProvider>
|
||||
<NotificationProvider>
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-[322p ] py-2.5">
|
||||
<section className="py-20">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<Card className="overflow-hidden bg-primary">
|
||||
<div className="grid lg:grid-cols-2 gap-0">
|
||||
|
|
|
|||
|
|
@ -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<Job[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(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')}
|
||||
</motion.h1>
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
|
|
@ -179,7 +181,7 @@ function JobsContent() {
|
|||
transition={{ delay: 0.1 }}
|
||||
className="text-lg text-muted-foreground text-pretty"
|
||||
>
|
||||
{loading ? "Carregando vagas..." : `${jobs.length} vagas disponíveis nas melhores empresas`}
|
||||
{loading ? t('jobs.loading') : t('jobs.subtitle', { count: jobs.length })}
|
||||
</motion.p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -194,7 +196,7 @@ function JobsContent() {
|
|||
<div className="flex-1 relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Buscar por cargo, empresa ou descrição..."
|
||||
placeholder={t('jobs.search')}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10 h-12"
|
||||
|
|
@ -208,7 +210,7 @@ function JobsContent() {
|
|||
className="h-12 gap-2"
|
||||
>
|
||||
<SlidersHorizontal className="h-4 w-4" />
|
||||
Filtros
|
||||
{t('jobs.filters.toggle')}
|
||||
{hasActiveFilters && (
|
||||
<Badge variant="secondary" className="ml-1 px-1 py-0 text-xs">
|
||||
!
|
||||
|
|
@ -233,10 +235,10 @@ function JobsContent() {
|
|||
<Select value={locationFilter} onValueChange={setLocationFilter}>
|
||||
<SelectTrigger>
|
||||
<MapPin className="h-4 w-4 mr-2" />
|
||||
<SelectValue placeholder="Localização" />
|
||||
<SelectValue placeholder={t('jobs.filters.location')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Todas as localizações</SelectItem>
|
||||
<SelectItem value="all">{t('jobs.filters.all')}</SelectItem>
|
||||
{uniqueLocations.map((location) => (
|
||||
<SelectItem key={location} value={location}>
|
||||
{location}
|
||||
|
|
@ -248,10 +250,10 @@ function JobsContent() {
|
|||
<Select value={typeFilter} onValueChange={setTypeFilter}>
|
||||
<SelectTrigger>
|
||||
<Briefcase className="h-4 w-4 mr-2" />
|
||||
<SelectValue placeholder="Tipo" />
|
||||
<SelectValue placeholder={t('jobs.filters.type')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Todos os tipos</SelectItem>
|
||||
<SelectItem value="all">{t('jobs.filters.all')}</SelectItem>
|
||||
{uniqueTypes.map((type) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
{type === "full-time" ? "Tempo integral" :
|
||||
|
|
@ -265,15 +267,15 @@ function JobsContent() {
|
|||
<Select value={workModeFilter} onValueChange={setWorkModeFilter}>
|
||||
<SelectTrigger>
|
||||
<MapPin className="h-4 w-4 mr-2" />
|
||||
<SelectValue placeholder="Modalidade" />
|
||||
<SelectValue placeholder={t('jobs.filters.workMode')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Todas as modalidades</SelectItem>
|
||||
<SelectItem value="all">{t('jobs.filters.all')}</SelectItem>
|
||||
{uniqueWorkModes.map((mode) => (
|
||||
<SelectItem key={mode} value={mode}>
|
||||
{mode === "remote" ? "Remoto" :
|
||||
mode === "hybrid" ? "Híbrido" :
|
||||
mode === "onsite" ? "Presencial" : mode}
|
||||
{mode === "remote" ? t('workMode.remote') :
|
||||
mode === "hybrid" ? t('workMode.hybrid') :
|
||||
mode === "onsite" ? t('workMode.onsite') : mode}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
|
@ -282,13 +284,13 @@ function JobsContent() {
|
|||
<Select value={sortBy} onValueChange={setSortBy}>
|
||||
<SelectTrigger>
|
||||
<ArrowUpDown className="h-4 w-4 mr-2" />
|
||||
<SelectValue placeholder="Ordenar por" />
|
||||
<SelectValue placeholder={t('jobs.filters.order')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="recent">Mais recentes</SelectItem>
|
||||
<SelectItem value="title">Título</SelectItem>
|
||||
<SelectItem value="company">Empresa</SelectItem>
|
||||
<SelectItem value="location">Localização</SelectItem>
|
||||
<SelectItem value="recent">{t('jobs.sort.recent')}</SelectItem>
|
||||
<SelectItem value="title">{t('jobs.sort.title')}</SelectItem>
|
||||
<SelectItem value="company">{t('jobs.sort.company')}</SelectItem>
|
||||
<SelectItem value="location">{t('jobs.sort.location')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
|
|
@ -299,7 +301,7 @@ function JobsContent() {
|
|||
className="gap-2"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
Limpar
|
||||
{t('jobs.reset')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -312,8 +314,11 @@ function JobsContent() {
|
|||
{/* Results Summary */}
|
||||
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
||||
<span>
|
||||
{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
|
||||
})}
|
||||
</span>
|
||||
{hasActiveFilters && (
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -370,7 +375,7 @@ function JobsContent() {
|
|||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center text-muted-foreground">Carregando vagas...</div>
|
||||
<div className="text-center text-muted-foreground">{t('jobs.loading')}</div>
|
||||
) : paginatedJobs.length > 0 ? (
|
||||
<div className="space-y-8">
|
||||
<motion.div layout className="grid gap-6">
|
||||
|
|
@ -398,17 +403,17 @@ function JobsContent() {
|
|||
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
Anterior
|
||||
{t('jobs.pagination.previous')}
|
||||
</Button>
|
||||
<div className="text-sm text-muted-foreground px-4">
|
||||
Página {currentPage} de {totalPages}
|
||||
{currentPage} / {totalPages}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
Próxima
|
||||
{t('jobs.pagination.next')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -421,9 +426,9 @@ function JobsContent() {
|
|||
>
|
||||
<div className="max-w-md mx-auto">
|
||||
<Search className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold mb-2">Nenhuma vaga encontrada</h3>
|
||||
<h3 className="text-lg font-semibold mb-2">{t('jobs.noResults.title')}</h3>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Não encontramos vagas que correspondam aos seus critérios de busca.
|
||||
{t('jobs.noResults.desc')}
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
|
|
@ -431,7 +436,7 @@ function JobsContent() {
|
|||
className="gap-2"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
Limpar filtros
|
||||
{t('jobs.resetFilters')}
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<footer className="border-t border-border bg-muted/30 mt-24">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
|
|
@ -10,12 +14,12 @@ export function Footer() {
|
|||
<span className="text-xl font-semibold">GoHorse Jobs</span>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm leading-relaxed max-w-md">
|
||||
Conectamos candidatos e empresas de forma rápida e direta. Encontre sua próxima oportunidade profissional em tecnologia.
|
||||
{t('home.hero.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold mb-4">Vagas por Tecnologia</h3>
|
||||
<h3 className="font-semibold mb-4">{t('footer.jobsByTech')}</h3>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
<li>
|
||||
<Link href="/vagas?tech=python" className="hover:text-foreground transition-colors">
|
||||
|
|
@ -34,44 +38,44 @@ export function Footer() {
|
|||
</li>
|
||||
<li>
|
||||
<Link href="/vagas?type=remoto" className="hover:text-foreground transition-colors">
|
||||
Vagas Remotas
|
||||
{t('workMode.remote')}
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold mb-4">Empresa</h3>
|
||||
<h3 className="font-semibold mb-4">{t('footer.company')}</h3>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
<li>
|
||||
<Link href="/sobre" className="hover:text-foreground transition-colors">
|
||||
Sobre
|
||||
{t('footer.about')}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/contato" className="hover:text-foreground transition-colors">
|
||||
Contato
|
||||
{t('nav.contact')}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/vagas" className="hover:text-foreground transition-colors">
|
||||
Todas as Vagas
|
||||
{t('nav.jobs')}
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold mb-4">Legal</h3>
|
||||
<h3 className="font-semibold mb-4">{t('footer.legal')}</h3>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
<li>
|
||||
<Link href="/privacidade" className="hover:text-foreground transition-colors">
|
||||
Política de Privacidade
|
||||
{t('footer.privacy')}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/termos" className="hover:text-foreground transition-colors">
|
||||
Termos de Uso
|
||||
{t('footer.terms')}
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
@ -79,7 +83,7 @@ export function Footer() {
|
|||
</div>
|
||||
|
||||
<div className="mt-12 pt-8 border-t border-border text-center text-sm text-muted-foreground">
|
||||
<p>© 2025 GoHorse Jobs. Todos os direitos reservados.</p>
|
||||
<p>{t('footer.copyright', { year: currentYear })}</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
|
|
|||
|
|
@ -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 })}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -200,11 +196,11 @@ export function JobCard({ job }: JobCardProps) {
|
|||
<div className="flex gap-2 w-full">
|
||||
<Link href={`/vagas/${job.id}`} className="flex-1">
|
||||
<Button variant="outline" className="w-full cursor-pointer">
|
||||
Ver detalhes
|
||||
{t('jobs.card.viewDetails')}
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href={`/vagas/${job.id}/candidatura`} className="flex-1">
|
||||
<Button className="w-full cursor-pointer">Candidatar-se</Button>
|
||||
<Button className="w-full cursor-pointer">{t('jobs.card.apply')}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</CardFooter>
|
||||
|
|
|
|||
|
|
@ -114,13 +114,13 @@ export function Navbar() {
|
|||
<Link href="/login" onClick={() => setIsOpen(false)}>
|
||||
<Button variant="ghost" className="w-full justify-start gap-2">
|
||||
<LogIn className="w-4 h-4" />
|
||||
Entrar
|
||||
{t('nav.login')}
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/cadastro/candidato" onClick={() => setIsOpen(false)}>
|
||||
<Button className="w-full justify-start gap-2">
|
||||
<User className="w-4 h-4" />
|
||||
Cadastrar
|
||||
{t('nav.register')}
|
||||
</Button>
|
||||
</Link>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ const dictionaries: Record<Locale, typeof en> = {
|
|||
const I18nContext = createContext<I18nContextType | null>(null);
|
||||
|
||||
export function I18nProvider({ children }: { children: ReactNode }) {
|
||||
const [locale, setLocale] = useState<Locale>('en');
|
||||
const [locale, setLocale] = useState<Locale>('pt-BR');
|
||||
|
||||
const t = useCallback((key: string, params?: Record<string, string | number>): string => {
|
||||
const keys = key.split('.');
|
||||
|
|
|
|||
Loading…
Reference in a new issue