W5.8: language toggle in the navbar (whole-site PT-BR / EN)
Some checks failed
CI / Web — typecheck + lint + build (push) Failing after 1m16s
CI / Scripts — Python smoke (push) Failing after 36s
CI / Web — npm audit (push) Failing after 28s
CI / Retrieval — golden set (Recall@5 + MRR) (push) Failing after 4s

User: "quero que o usuário possa escolher o idioma na navbar, e então leia
o site todo em seu idioma."

The locale infrastructure already existed (cookie + getLocale() + every
page renders bilingual). The toggle only lived as a subtle floating
control bottom-left. This surfaces it in the navbars where users expect
it, and confirms whole-site language switching.

  locale-toggle-client.tsx
    - New variant="navbar": gold-amber pill matching the magazine theme
      (PT | EN, active = solid gold). Retro green variant kept for the
      floating fallback.
    - Guard: clicking the already-active language is a no-op.
    - aria-pressed on each button for a11y.

  locale-toggle.tsx: LocaleToggle gains variant passthrough.

  site-header.tsx: <LocaleToggle variant="navbar"> added to the main nav
    (homepage + /sightings /witnesses /objects /locations /operations
    /documents).

  bureau-nav.tsx: optional `locale` prop renders the same toggle pushed
    to the right (ml-auto) on /bureau, /c/[slug], /h/[id], /jobs/[id].
    All four callers now pass locale (getLocale added to h + jobs pages).

Cookie-based: switching sets `locale` cookie + router.refresh(); the
server re-renders every component through getLocale(), so the entire
page — headers, hero, summaries, case narratives, entity cards — flips
language in one click. The floating bottom-left toggle stays as a global
fallback for pages without a navbar (/d, /e, /search, /graph).

Also this session: Sun-Tzu smoke succeeded on the refreshed
CLAUDE_CODE_OAUTH_TOKEN — brief B-0001 written for the green fireballs
("bolas de fogo verdes confinadas ao corredor nuclear... cor espectral
e trajetória quase horizontal que os próprios analistas consideraram
irreconciliáveis com meteoros naturais", 3 pillars). Bulk entity
enrichment resumed in the background.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Luiz Gustavo 2026-05-24 22:54:37 -03:00
parent df82d40a96
commit 3e88a64933
9 changed files with 53 additions and 10 deletions

View file

@ -17,7 +17,7 @@ export default async function BureauPage() {
return ( return (
<div className="min-h-screen bg-[#0a0e1a] text-[#e7ecf3]"> <div className="min-h-screen bg-[#0a0e1a] text-[#e7ecf3]">
<BureauNav crumbs={[{ label: locale === "en" ? "case files" : "casos" }]} /> <BureauNav locale={locale} crumbs={[{ label: locale === "en" ? "case files" : "casos" }]} />
<AuthBar /> <AuthBar />
<div className="mx-auto max-w-5xl px-4 py-6 pt-4"> <div className="mx-auto max-w-5xl px-4 py-6 pt-4">
<header className="mb-6 border-b border-[rgba(224,192,128,0.32)] pb-4"> <header className="mb-6 border-b border-[rgba(224,192,128,0.32)] pb-4">

View file

@ -131,7 +131,7 @@ export default async function CaseReportPage({
return ( return (
<div className="min-h-screen bg-[#0a0e1a] text-[#e7ecf3]"> <div className="min-h-screen bg-[#0a0e1a] text-[#e7ecf3]">
<BureauNav crumbs={[ <BureauNav locale={locale} crumbs={[
{ label: locale === "en" ? "case files" : "casos", href: "/bureau" }, { label: locale === "en" ? "case files" : "casos", href: "/bureau" },
{ label: title.length > 32 ? title.slice(0, 32) + "…" : title }, { label: title.length > 32 ? title.slice(0, 32) + "…" : title },
]} /> ]} />

View file

@ -39,7 +39,7 @@ export default async function DocumentsPage() {
return ( return (
<div className="min-h-screen"> <div className="min-h-screen">
<SiteHeader locale={locale} /> <SiteHeader locale={locale} />
<BureauNav crumbs={[{ label: locale === "en" ? "documents" : "documentos" }]} /> <BureauNav locale={locale} crumbs={[{ label: locale === "en" ? "documents" : "documentos" }]} />
<div className="mx-auto max-w-7xl px-4 md:px-8 py-10 md:py-14"> <div className="mx-auto max-w-7xl px-4 md:px-8 py-10 md:py-14">
<header className="mb-10"> <header className="mb-10">

View file

@ -18,6 +18,7 @@ import path from "node:path";
import { pgQuery } from "@/lib/retrieval/db"; import { pgQuery } from "@/lib/retrieval/db";
import { AuthBar } from "@/components/auth-bar"; import { AuthBar } from "@/components/auth-bar";
import { BureauNav } from "@/components/bureau-nav"; import { BureauNav } from "@/components/bureau-nav";
import { getLocale } from "@/components/locale-toggle";
import { RedTeamRequestButton } from "@/components/red-team-request-button"; import { RedTeamRequestButton } from "@/components/red-team-request-button";
export const runtime = "nodejs"; export const runtime = "nodejs";
@ -149,6 +150,7 @@ export default async function HypothesisPage({
}: { params: Promise<{ hypothesisId: string }> }) { }: { params: Promise<{ hypothesisId: string }> }) {
const { hypothesisId } = await params; const { hypothesisId } = await params;
if (!/^H-\d{4}$/.test(hypothesisId)) notFound(); if (!/^H-\d{4}$/.test(hypothesisId)) notFound();
const navLocale = await getLocale();
const rows = await pgQuery<HypothesisRow>( const rows = await pgQuery<HypothesisRow>(
`SELECT hypothesis_id, question, question_pt_br, position, position_pt_br, `SELECT hypothesis_id, question, question_pt_br, position, position_pt_br,
@ -197,7 +199,7 @@ export default async function HypothesisPage({
return ( return (
<div className="min-h-screen bg-[#0a0e1a] text-[#e7ecf3]"> <div className="min-h-screen bg-[#0a0e1a] text-[#e7ecf3]">
<BureauNav crumbs={[ <BureauNav locale={navLocale} crumbs={[
{ label: "bureau", href: "/bureau" }, { label: "bureau", href: "/bureau" },
{ label: "hypotheses", href: "/bureau#hypotheses" }, { label: "hypotheses", href: "/bureau#hypotheses" },
{ label: hypothesisId }, { label: hypothesisId },

View file

@ -19,6 +19,7 @@ import Link from "next/link";
import { pgQuery } from "@/lib/retrieval/db"; import { pgQuery } from "@/lib/retrieval/db";
import { AuthBar } from "@/components/auth-bar"; import { AuthBar } from "@/components/auth-bar";
import { BureauNav } from "@/components/bureau-nav"; import { BureauNav } from "@/components/bureau-nav";
import { getLocale } from "@/components/locale-toggle";
import { JobStatusPoller } from "@/components/job-status-poller"; import { JobStatusPoller } from "@/components/job-status-poller";
export const runtime = "nodejs"; export const runtime = "nodejs";
@ -42,6 +43,7 @@ export default async function JobPage({
}: { params: Promise<{ id: string }> }) { }: { params: Promise<{ id: string }> }) {
const { id } = await params; const { id } = await params;
if (!/^[0-9a-f-]{36}$/i.test(id)) notFound(); if (!/^[0-9a-f-]{36}$/i.test(id)) notFound();
const navLocale = await getLocale();
const rows = await pgQuery<InitialJob>( const rows = await pgQuery<InitialJob>(
`SELECT job_id, kind, payload, status, worker_id, started_at, finished_at, `SELECT job_id, kind, payload, status, worker_id, started_at, finished_at,
@ -76,7 +78,7 @@ export default async function JobPage({
return ( return (
<div className="min-h-screen bg-[#0a0e1a] text-[#e7ecf3]"> <div className="min-h-screen bg-[#0a0e1a] text-[#e7ecf3]">
<BureauNav crumbs={[ <BureauNav locale={navLocale} crumbs={[
{ label: "bureau", href: "/bureau" }, { label: "bureau", href: "/bureau" },
{ label: "jobs", href: "/bureau#jobs" }, { label: "jobs", href: "/bureau#jobs" },
{ label: job.job_id.slice(0, 8) }, { label: job.job_id.slice(0, 8) },

View file

@ -11,6 +11,7 @@
*/ */
import Link from "next/link"; import Link from "next/link";
import { ArrowLeft, Home } from "lucide-react"; import { ArrowLeft, Home } from "lucide-react";
import { LocaleToggle, type Locale } from "./locale-toggle";
export interface Crumb { export interface Crumb {
/** Display label. */ /** Display label. */
@ -19,7 +20,7 @@ export interface Crumb {
href?: string; href?: string;
} }
export function BureauNav({ crumbs }: { crumbs: Crumb[] }) { export function BureauNav({ crumbs, locale }: { crumbs: Crumb[]; locale?: Locale }) {
return ( return (
<nav className="sticky top-0 z-30 bg-[#0a0e1a]/95 backdrop-blur border-b border-[rgba(224,192,128,0.18)]"> <nav className="sticky top-0 z-30 bg-[#0a0e1a]/95 backdrop-blur border-b border-[rgba(224,192,128,0.18)]">
<div className="mx-auto max-w-6xl px-4 py-2 flex items-center gap-3 flex-wrap text-[12px] font-mono"> <div className="mx-auto max-w-6xl px-4 py-2 flex items-center gap-3 flex-wrap text-[12px] font-mono">
@ -56,6 +57,12 @@ export function BureauNav({ crumbs }: { crumbs: Crumb[] }) {
))} ))}
</div> </div>
)} )}
{locale && (
<span className="ml-auto">
<LocaleToggle current={locale} variant="navbar" />
</span>
)}
</div> </div>
</nav> </nav>
); );

View file

@ -4,14 +4,41 @@ import { useRouter } from "next/navigation";
import { Globe } from "lucide-react"; import { Globe } from "lucide-react";
import type { Locale } from "./locale-toggle"; import type { Locale } from "./locale-toggle";
export function LocaleToggleClient({ current }: { current: Locale }) { export function LocaleToggleClient({
current,
variant = "retro",
}: { current: Locale; variant?: "retro" | "navbar" }) {
const router = useRouter(); const router = useRouter();
function setLocale(next: Locale) { function setLocale(next: Locale) {
if (next === current) return;
document.cookie = `locale=${next}; path=/; max-age=${60 * 60 * 24 * 365}; SameSite=Lax`; document.cookie = `locale=${next}; path=/; max-age=${60 * 60 * 24 * 365}; SameSite=Lax`;
router.refresh(); router.refresh();
} }
if (variant === "navbar") {
// Gold-amber magazine theme to match SiteHeader / BureauNav.
return (
<div className="inline-flex items-center gap-0.5 font-mono text-[11px] uppercase tracking-wider border border-[rgba(224,192,128,0.30)] rounded-full overflow-hidden">
<span className="pl-2.5 pr-1 text-[#5a6678]"><Globe size={11} /></span>
<button
onClick={() => setLocale("pt-br")}
aria-pressed={current === "pt-br"}
className={`px-2 py-1 transition-colors ${current === "pt-br" ? "bg-[#e0c080] text-[#0a0e1a] font-semibold" : "text-[#9aa6b8] hover:text-[#e0c080]"}`}
>
PT
</button>
<button
onClick={() => setLocale("en")}
aria-pressed={current === "en"}
className={`px-2 py-1 transition-colors ${current === "en" ? "bg-[#e0c080] text-[#0a0e1a] font-semibold" : "text-[#9aa6b8] hover:text-[#e0c080]"}`}
>
EN
</button>
</div>
);
}
return ( return (
<div className="inline-flex items-center gap-1 font-mono text-[10px] uppercase tracking-widest border border-[rgba(0,255,156,0.32)] rounded"> <div className="inline-flex items-center gap-1 font-mono text-[10px] uppercase tracking-widest border border-[rgba(0,255,156,0.32)] rounded">
<span className="px-2 py-1 text-[#5a6678]"><Globe size={11} /></span> <span className="px-2 py-1 text-[#5a6678]"><Globe size={11} /></span>

View file

@ -13,6 +13,9 @@ export async function getLocale(): Promise<Locale> {
return v === "en" ? "en" : "pt-br"; return v === "en" ? "en" : "pt-br";
} }
export function LocaleToggle({ current }: { current: Locale }) { export function LocaleToggle({
return <LocaleToggleClient current={current} />; current,
variant = "retro",
}: { current: Locale; variant?: "retro" | "navbar" }) {
return <LocaleToggleClient current={current} variant={variant} />;
} }

View file

@ -6,6 +6,7 @@
*/ */
import Link from "next/link"; import Link from "next/link";
import { AuthBar } from "./auth-bar"; import { AuthBar } from "./auth-bar";
import { LocaleToggle } from "./locale-toggle";
export function SiteHeader({ locale }: { locale: "pt-br" | "en" }) { export function SiteHeader({ locale }: { locale: "pt-br" | "en" }) {
return ( return (
@ -26,7 +27,8 @@ export function SiteHeader({ locale }: { locale: "pt-br" | "en" }) {
<NavLink href="/operations" label={locale === "en" ? "Programs" : "Programas"} /> <NavLink href="/operations" label={locale === "en" ? "Programs" : "Programas"} />
<NavLink href="/bureau" label={locale === "en" ? "Case files" : "Casos"} /> <NavLink href="/bureau" label={locale === "en" ? "Case files" : "Casos"} />
<NavLink href="/search" label={locale === "en" ? "Search" : "Busca"} /> <NavLink href="/search" label={locale === "en" ? "Search" : "Busca"} />
<span className="ml-2"><AuthBar /></span> <span className="ml-1"><LocaleToggle current={locale} variant="navbar" /></span>
<span className="ml-1"><AuthBar /></span>
</nav> </nav>
</div> </div>
</header> </header>