W5.8: language toggle in the navbar (whole-site PT-BR / EN)
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:
parent
df82d40a96
commit
3e88a64933
9 changed files with 53 additions and 10 deletions
|
|
@ -17,7 +17,7 @@ export default async function BureauPage() {
|
|||
|
||||
return (
|
||||
<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 />
|
||||
<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">
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ export default async function CaseReportPage({
|
|||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a0e1a] text-[#e7ecf3]">
|
||||
<BureauNav crumbs={[
|
||||
<BureauNav locale={locale} crumbs={[
|
||||
{ label: locale === "en" ? "case files" : "casos", href: "/bureau" },
|
||||
{ label: title.length > 32 ? title.slice(0, 32) + "…" : title },
|
||||
]} />
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export default async function DocumentsPage() {
|
|||
return (
|
||||
<div className="min-h-screen">
|
||||
<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">
|
||||
<header className="mb-10">
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import path from "node:path";
|
|||
import { pgQuery } from "@/lib/retrieval/db";
|
||||
import { AuthBar } from "@/components/auth-bar";
|
||||
import { BureauNav } from "@/components/bureau-nav";
|
||||
import { getLocale } from "@/components/locale-toggle";
|
||||
import { RedTeamRequestButton } from "@/components/red-team-request-button";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
|
@ -149,6 +150,7 @@ export default async function HypothesisPage({
|
|||
}: { params: Promise<{ hypothesisId: string }> }) {
|
||||
const { hypothesisId } = await params;
|
||||
if (!/^H-\d{4}$/.test(hypothesisId)) notFound();
|
||||
const navLocale = await getLocale();
|
||||
|
||||
const rows = await pgQuery<HypothesisRow>(
|
||||
`SELECT hypothesis_id, question, question_pt_br, position, position_pt_br,
|
||||
|
|
@ -197,7 +199,7 @@ export default async function HypothesisPage({
|
|||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a0e1a] text-[#e7ecf3]">
|
||||
<BureauNav crumbs={[
|
||||
<BureauNav locale={navLocale} crumbs={[
|
||||
{ label: "bureau", href: "/bureau" },
|
||||
{ label: "hypotheses", href: "/bureau#hypotheses" },
|
||||
{ label: hypothesisId },
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import Link from "next/link";
|
|||
import { pgQuery } from "@/lib/retrieval/db";
|
||||
import { AuthBar } from "@/components/auth-bar";
|
||||
import { BureauNav } from "@/components/bureau-nav";
|
||||
import { getLocale } from "@/components/locale-toggle";
|
||||
import { JobStatusPoller } from "@/components/job-status-poller";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
|
@ -42,6 +43,7 @@ export default async function JobPage({
|
|||
}: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
if (!/^[0-9a-f-]{36}$/i.test(id)) notFound();
|
||||
const navLocale = await getLocale();
|
||||
|
||||
const rows = await pgQuery<InitialJob>(
|
||||
`SELECT job_id, kind, payload, status, worker_id, started_at, finished_at,
|
||||
|
|
@ -76,7 +78,7 @@ export default async function JobPage({
|
|||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a0e1a] text-[#e7ecf3]">
|
||||
<BureauNav crumbs={[
|
||||
<BureauNav locale={navLocale} crumbs={[
|
||||
{ label: "bureau", href: "/bureau" },
|
||||
{ label: "jobs", href: "/bureau#jobs" },
|
||||
{ label: job.job_id.slice(0, 8) },
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
*/
|
||||
import Link from "next/link";
|
||||
import { ArrowLeft, Home } from "lucide-react";
|
||||
import { LocaleToggle, type Locale } from "./locale-toggle";
|
||||
|
||||
export interface Crumb {
|
||||
/** Display label. */
|
||||
|
|
@ -19,7 +20,7 @@ export interface Crumb {
|
|||
href?: string;
|
||||
}
|
||||
|
||||
export function BureauNav({ crumbs }: { crumbs: Crumb[] }) {
|
||||
export function BureauNav({ crumbs, locale }: { crumbs: Crumb[]; locale?: Locale }) {
|
||||
return (
|
||||
<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">
|
||||
|
|
@ -56,6 +57,12 @@ export function BureauNav({ crumbs }: { crumbs: Crumb[] }) {
|
|||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{locale && (
|
||||
<span className="ml-auto">
|
||||
<LocaleToggle current={locale} variant="navbar" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,14 +4,41 @@ import { useRouter } from "next/navigation";
|
|||
import { Globe } from "lucide-react";
|
||||
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();
|
||||
|
||||
function setLocale(next: Locale) {
|
||||
if (next === current) return;
|
||||
document.cookie = `locale=${next}; path=/; max-age=${60 * 60 * 24 * 365}; SameSite=Lax`;
|
||||
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 (
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ export async function getLocale(): Promise<Locale> {
|
|||
return v === "en" ? "en" : "pt-br";
|
||||
}
|
||||
|
||||
export function LocaleToggle({ current }: { current: Locale }) {
|
||||
return <LocaleToggleClient current={current} />;
|
||||
export function LocaleToggle({
|
||||
current,
|
||||
variant = "retro",
|
||||
}: { current: Locale; variant?: "retro" | "navbar" }) {
|
||||
return <LocaleToggleClient current={current} variant={variant} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
import Link from "next/link";
|
||||
import { AuthBar } from "./auth-bar";
|
||||
import { LocaleToggle } from "./locale-toggle";
|
||||
|
||||
export function SiteHeader({ locale }: { locale: "pt-br" | "en" }) {
|
||||
return (
|
||||
|
|
@ -26,7 +27,8 @@ export function SiteHeader({ locale }: { locale: "pt-br" | "en" }) {
|
|||
<NavLink href="/operations" label={locale === "en" ? "Programs" : "Programas"} />
|
||||
<NavLink href="/bureau" label={locale === "en" ? "Case files" : "Casos"} />
|
||||
<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>
|
||||
</div>
|
||||
</header>
|
||||
|
|
|
|||
Loading…
Reference in a new issue