Files
zeekayandGitHub 949608fd9d i18n: pages and components c–l draw their copy through @hanzo/i18n (#67)
Calculator, captable, careers, cloud, compare, contact and contact-sales,
customers, dashboards, dataroom, datastore, dev, download, enso,
enterprise, extension, family, integrations, investors, kai, learn,
legal and their components say their words through t() in client code,
<T> in server components and phrase() where copy is held as data; one
sentence is one key, figures and links its arguments.

Rebased on main: in every conflict main's file stands (its copy, layout
and styling) and only the wrapping is added back, including copy main
added since.

Claude-Session: https://claude.ai/code/session_015XYKLjZtDTJEnMo4eK3AFH
2026-10-30 01:15:31 -07:00

104 lines
4.4 KiB
TypeScript

import { notFound } from 'next/navigation'
import { Page, PageHero, Prose, Section } from '@/components/marketing/page-kit'
import { legalDoc } from '@/lib/legal'
import { pageMeta } from '@/lib/page-meta'
import { AUP_VERSION, TERMS_VERSION, versionDate } from '@hanzo/app/lib/terms'
import { phrase } from '@hanzo/i18n'
import { T } from '@hanzo/i18n/react'
/** The two documents a person agrees to at sign-up take their version from `@hanzo/app/lib/terms`, not their own front matter. */
const AGREED: Record<string, string> = { terms: TERMS_VERSION, 'acceptable-use': AUP_VERSION }
/**
* A legal document, rendered — the ONE place any of them is drawn.
*
* `content/legal/*.md` is the one authoritative version of each document and
* this is the one thing that renders it. Four routes reach it: `/legal/<slug>`
* for all ten, and `/privacy`, `/terms` and `/cookies` for the three whose
* short addresses were minted before the markdown stack existed.
*
* Those three used to be HAND-WRITTEN TSX, and they were not stale renderings
* of the same text — they were DIFFERENT DOCUMENTS. The TSX privacy policy ran
* thirteen generic sections against the markdown's nineteen, and the two
* disagreed about the minimum age and about whether research participation is
* on by default. A site cannot serve two answers to "what are your terms", so
* the bodies are gone and both addresses now read one file.
*
* The status line is not decoration. Every one of these is a DRAFT pending
* counsel, and the page says so above the text, from the document's own front
* matter. A reviewer relying on unexecuted terms because a page implied
* otherwise is the exact failure /trust exists to avoid.
*/
export function LegalDocument({ slug }: { slug: string }) {
const doc = legalDoc(slug)
if (!doc) notFound()
const effective = doc.status.toLowerCase() === 'effective'
const version = AGREED[slug] ?? doc.version
const date = AGREED[slug] ? versionDate(version) : doc.updated
return (
<Page>
<PageHero eyebrow={phrase('Legal')} title={doc.title} lede={doc.summary} />
<Section>
<Prose>
{effective ? null : (
<p>
<strong><T>Draft.</T></strong>{' '}<T>This document is not yet in force.</T>
</p>
)}
<p>
<T values={{ date, version }}>{'Effective {date} · Version {version} ·'}</T>{' '}
{doc.previous ? <a href={doc.previous}><T>Previous version</T></a> : <T>Previous version: none, this is the first.</T>}
</p>
{doc.toc.length > 1 ? (
<nav aria-label="Contents">
<h2><T>Contents</T></h2>
<ul>
{doc.toc.map((row) => (
<li key={row.id}>
<a href={`#${row.id}`}>{row.text}</a>
</li>
))}
</ul>
</nav>
) : null}
</Prose>
</Section>
<Section>
<Prose html={doc.html} />
</Section>
</Page>
)
}
/**
* The metadata for a document served at its own short address, `/<slug>`.
*
* `/legal/<slug>` is the address, and this is what says so: the canonical
* points there, so search consolidates two URLs onto one document rather than
* ranking them against each other. `pageMeta` already carries that option and
* already states the reason — "left to point at itself it is a second, empty
* URL competing with the page that holds the words".
*
* A redirect stub was the alternative and it is not available here. This is a
* static export, so Next's `redirects()` emits nothing; the repo's substitute
* is a shell listed in `lib/publish`'s `EMPTY`, which makes the route
* `noindex` — and `shown()` in `components/home/shell.tsx` links a page in the
* chrome only while `policy()` calls it public. `@hanzogui/shell`'s footer
* registry points at `https://hanzo.ai/privacy` and `https://hanzo.ai/terms`,
* so a stub would delete both links from the footer of every page on the site,
* in a package this repo cannot edit.
*
* Title and description come from the document's own front matter, so nothing
* about it is typed twice.
*/
export function legalMeta(slug: string, path = `/${slug}`) {
const doc = legalDoc(slug)
if (!doc) throw new Error(`no legal document '${slug}' in content/legal`)
return pageMeta({
title: doc.title,
description: doc.summary,
path,
canonical: `/legal/${slug}`,
})
}