Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 10 additions & 15 deletions web/app/[locale]/legal/privacy/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,30 @@ import Link from "next/link";
import { UsagePreferenceControl } from "@/components/usage-counting";
import { USAGE_COUNTING_COPY } from "@/lib/content/usage-counting";
import { BUILD_FACTS } from "@/lib/facts";
import { pickText } from "@/lib/i18n/dictionaries";
import { fill, getLegalPrivacy, pickText } from "@/lib/i18n/dictionaries";
import { buildPageMetadata } from "@/lib/page-meta";
import { LEGAL_UPDATED, PRIVACY_SECTIONS } from "@/lib/legal-copy";

export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
const isZh = locale === "zh";
const t = getLegalPrivacy(locale);
return buildPageMetadata({
path: "/legal/privacy",
locale,
title: isZh ? "隐私政策 · Codewhale" : "Privacy policy · Codewhale",
description: isZh
? "Shannon Labs 如何在你使用 Codewhale 时处理信息。"
: "How Shannon Labs handles information when you use Codewhale.",
title: t.metaTitle,
description: t.metaDescription,
});
}

export default async function PrivacyPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
const isZh = locale === "zh";
const t = getLegalPrivacy(locale);
return (
<div className="portal-home">
<article className="legal-doc">
<p className="legal-doc-kicker">{isZh ? "法律" : "Legal"}</p>
<h1>{isZh ? "隐私政策" : "Privacy policy"}</h1>
<p className="legal-doc-updated">
{isZh ? "生效并最近更新于" : "Effective and last updated"} {LEGAL_UPDATED}
{isZh ? "。以下为具有约束力的英文文本。" : "."}
</p>
<p className="legal-doc-kicker">{t.kicker}</p>
<h1>{t.title}</h1>
<p className="legal-doc-updated">{fill(t.updated, { date: LEGAL_UPDATED })}</p>
<p>This policy explains how Shannon Labs handles information when you use Codewhale.</p>
{PRIVACY_SECTIONS.map((section) => (
<section key={section.title}>
Expand All @@ -43,8 +38,8 @@ export default async function PrivacyPage({ params }: { params: Promise<{ locale
<UsagePreferenceControl locale={locale} appVersion={BUILD_FACTS.version ?? "0.0.0"} />
</section>
<p className="legal-doc-nav">
<Link href={`/${locale}/legal/terms`}>{isZh ? "服务条款" : "Terms of service"}</Link>
<Link href={`/${locale}`}>{isZh ? "返回首页" : "Back home"}</Link>
<Link href={`/${locale}/legal/terms`}>{t.termsLink}</Link>
<Link href={`/${locale}`}>{t.homeLink}</Link>
</p>
</article>
</div>
Expand Down
24 changes: 10 additions & 14 deletions web/app/[locale]/legal/terms/page.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,28 @@
import Link from "next/link";
import { fill, getLegalTerms } from "@/lib/i18n/dictionaries";
import { buildPageMetadata } from "@/lib/page-meta";
import { LEGAL_UPDATED, TERMS_SECTIONS } from "@/lib/legal-copy";

export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
const isZh = locale === "zh";
const t = getLegalTerms(locale);
return buildPageMetadata({
path: "/legal/terms",
locale,
title: isZh ? "服务条款 · Codewhale" : "Terms of service · Codewhale",
description: isZh
? "Shannon Labs 产品 Codewhale 的服务条款。"
: "Terms that govern your use of Codewhale, a Shannon Labs product.",
title: t.metaTitle,
description: t.metaDescription,
});
}

export default async function TermsPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
const isZh = locale === "zh";
const t = getLegalTerms(locale);
return (
<div className="portal-home">
<article className="legal-doc">
<p className="legal-doc-kicker">{isZh ? "法律" : "Legal"}</p>
<h1>{isZh ? "服务条款" : "Terms of service"}</h1>
<p className="legal-doc-updated">
{isZh ? "生效并最近更新于" : "Effective and last updated"} {LEGAL_UPDATED}
{isZh ? "。以下为具有约束力的英文文本。" : "."}
</p>
<p className="legal-doc-kicker">{t.kicker}</p>
<h1>{t.title}</h1>
<p className="legal-doc-updated">{fill(t.updated, { date: LEGAL_UPDATED })}</p>
<p>
These terms govern your use of Codewhale, a Shannon Labs product. By creating an
account or using the service, you agree to them.
Expand All @@ -38,8 +34,8 @@ export default async function TermsPage({ params }: { params: Promise<{ locale:
</section>
))}
<p className="legal-doc-nav">
<Link href={`/${locale}/legal/privacy`}>{isZh ? "隐私政策" : "Privacy policy"}</Link>
<Link href={`/${locale}`}>{isZh ? "返回首页" : "Back home"}</Link>
<Link href={`/${locale}/legal/privacy`}>{t.privacyLink}</Link>
<Link href={`/${locale}`}>{t.homeLink}</Link>
</p>
</article>
</div>
Expand Down
16 changes: 16 additions & 0 deletions web/lib/i18n/dictionaries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
EN_DOCS_SHELL,
EN_DOCS_TROUBLESHOOTING,
EN_HOME,
EN_LEGAL_PRIVACY,
EN_LEGAL_TERMS,
fill,
getChrome,
getDocsGuide,
Expand All @@ -38,6 +40,8 @@ import {
getDocsShell,
getDocsTroubleshooting,
getHome,
getLegalPrivacy,
getLegalTerms,
pickText,
splitToken,
splitTokens,
Expand Down Expand Up @@ -270,6 +274,8 @@ describe("website dictionaries", () => {
["docs-auth", getDocsAuth, EN_DOCS_AUTH],
["docs-trust", getDocsTrust, EN_DOCS_TRUST],
["changelog", getChangelog, EN_CHANGELOG],
["legal-terms", getLegalTerms, EN_LEGAL_TERMS],
["legal-privacy", getLegalPrivacy, EN_LEGAL_PRIVACY],
] as const) {
const enKeys = Object.keys(reference).sort();
for (const locale of [...DICTIONARY_LOCALES, "fr", "und"]) {
Expand Down Expand Up @@ -604,6 +610,16 @@ describe("website dictionaries", () => {
}
});

it("carries {date} through both legal pages' effective-date line", () => {
// The date used to sit in the JSX between two translated fragments. Now
// it is a fill() token, so a translation that drops it would ship a legal
// page that no longer says when it took effect.
for (const locale of ["en", "zh"]) {
expect(getLegalTerms(locale).updated, `${locale} legal-terms`).toContain("{date}");
expect(getLegalPrivacy(locale).updated, `${locale} legal-privacy`).toContain("{date}");
}
});

it("interpolates templates with fill() and leaves unknown tokens visible", () => {
expect(fill("Latest release {tag}", { tag: "v0.9.2" })).toBe("Latest release v0.9.2");
expect(fill("{count} provider routes", { count: 30 })).toBe("30 provider routes");
Expand Down
17 changes: 17 additions & 0 deletions web/lib/i18n/dictionaries/en/legal-privacy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { LegalPrivacyDict } from "../types";

/**
* English reference dictionary for `app/[locale]/legal/privacy/page.tsx`.
* Copy moved verbatim from the page's `isZh` ternaries — any wording change
* belongs in its own commit, never mixed into a structural move. The binding
* legal text itself is English for every locale and stays in `lib/legal-copy`.
*/
export const legalPrivacy: LegalPrivacyDict = {
metaTitle: "Privacy policy · Codewhale",
metaDescription: "How Shannon Labs handles information when you use Codewhale.",
kicker: "Legal",
title: "Privacy policy",
updated: "Effective and last updated {date}.",
termsLink: "Terms of service",
homeLink: "Back home",
};
17 changes: 17 additions & 0 deletions web/lib/i18n/dictionaries/en/legal-terms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { LegalTermsDict } from "../types";

/**
* English reference dictionary for `app/[locale]/legal/terms/page.tsx`.
* Copy moved verbatim from the page's `isZh` ternaries — any wording change
* belongs in its own commit, never mixed into a structural move. The binding
* legal text itself is English for every locale and stays in `lib/legal-copy`.
*/
export const legalTerms: LegalTermsDict = {
metaTitle: "Terms of service · Codewhale",
metaDescription: "Terms that govern your use of Codewhale, a Shannon Labs product.",
kicker: "Legal",
title: "Terms of service",
updated: "Effective and last updated {date}.",
privacyLink: "Privacy policy",
homeLink: "Back home",
};
30 changes: 27 additions & 3 deletions web/lib/i18n/dictionaries/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import type {
DocsWebDict,
DocsWorkDict,
HomeDict,
LegalPrivacyDict,
LegalTermsDict,
StatesDict,
} from "./types";
import { chrome as enChrome } from "./en/chrome";
Expand Down Expand Up @@ -94,6 +96,10 @@ import { states as enStates } from "./en/states";
import { states as zhStates } from "./zh/states";
import { changelog as enChangelog } from "./en/changelog";
import { changelog as zhChangelog } from "./zh/changelog";
import { legalTerms as enLegalTerms } from "./en/legal-terms";
import { legalTerms as zhLegalTerms } from "./zh/legal-terms";
import { legalPrivacy as enLegalPrivacy } from "./en/legal-privacy";
import { legalPrivacy as zhLegalPrivacy } from "./zh/legal-privacy";
import { chrome as zhChrome } from "./zh/chrome";
import { home as zhHome } from "./zh/home";
import { chrome as jaChrome } from "./ja/chrome";
Expand Down Expand Up @@ -284,9 +290,9 @@ const DOCS_TRUST: Record<string, DocsTrustDict> = {
};

/**
* Shared surface states and the changelog page follow the same optional
* per-locale rule as the docs page dictionaries: English is the reference,
* every other locale falls back to it at lookup time.
* Shared surface states, the changelog page and the two legal pages follow
* the same optional per-locale rule as the docs page dictionaries: English is
* the reference, every other locale falls back to it at lookup time.
*/
const STATES: Record<string, StatesDict> = {
zh: zhStates,
Expand All @@ -296,6 +302,14 @@ const CHANGELOG: Record<string, ChangelogDict> = {
zh: zhChangelog,
};

const LEGAL_TERMS: Record<string, LegalTermsDict> = {
zh: zhLegalTerms,
};

const LEGAL_PRIVACY: Record<string, LegalPrivacyDict> = {
zh: zhLegalPrivacy,
};

export function getChrome(locale: string): ChromeDict {
return CHROME[locale] ?? enChrome;
}
Expand Down Expand Up @@ -384,6 +398,14 @@ export function getChangelog(locale: string): ChangelogDict {
return CHANGELOG[locale] ?? enChangelog;
}

export function getLegalTerms(locale: string): LegalTermsDict {
return LEGAL_TERMS[locale] ?? enLegalTerms;
}

export function getLegalPrivacy(locale: string): LegalPrivacyDict {
return LEGAL_PRIVACY[locale] ?? enLegalPrivacy;
}

/**
* Select one side of a legacy `{ en, zh }` content pair by locale. This is
* the transitional bridge for `web/lib/content/` modules that still carry
Expand Down Expand Up @@ -418,6 +440,8 @@ export const EN_DOCS_TRUST = enDocsTrust;
export const EN_COMPUTER_USE = enComputerUse;
export const EN_STATES = enStates;
export const EN_CHANGELOG = enChangelog;
export const EN_LEGAL_TERMS = enLegalTerms;
export const EN_LEGAL_PRIVACY = enLegalPrivacy;

/** Interpolate `{name}` tokens in a dictionary template. Unknown tokens are
* left intact so a template/variable drift is visible in review, not silent. */
Expand Down
22 changes: 22 additions & 0 deletions web/lib/i18n/dictionaries/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,28 @@ export interface ChangelogDict {
emptyBody: string;
}

export interface LegalTermsDict {
metaTitle: string;
metaDescription: string;
kicker: string;
title: string;
/** "Effective and last updated {date}." — zh also says the English text binds. */
updated: string;
privacyLink: string;
homeLink: string;
}

export interface LegalPrivacyDict {
metaTitle: string;
metaDescription: string;
kicker: string;
title: string;
/** Same template as `LegalTermsDict.updated`. */
updated: string;
termsLink: string;
homeLink: string;
}

/**
* `app/[locale]/docs/hooks/page.tsx`.
*
Expand Down
21 changes: 21 additions & 0 deletions web/lib/i18n/dictionaries/zh/legal-privacy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { LegalPrivacyDict } from "../types";

/**
* Chinese dictionary for `app/[locale]/legal/privacy/page.tsx`. Only the page
* chrome is translated; the terms themselves are binding in English, which is
* what `updated` tells the reader.
*
* Spacing note: the space between `于` and `{date}` came from the literal
* space between two JSX expressions, and there is none before `。` because
* JSX dropped the newline-only whitespace that separated them. Do not "fix"
* either as a typo.
*/
export const legalPrivacy: LegalPrivacyDict = {
metaTitle: "隐私政策 · Codewhale",
metaDescription: "Shannon Labs 如何在你使用 Codewhale 时处理信息。",
kicker: "法律",
title: "隐私政策",
updated: "生效并最近更新于 {date}。以下为具有约束力的英文文本。",
termsLink: "服务条款",
homeLink: "返回首页",
};
21 changes: 21 additions & 0 deletions web/lib/i18n/dictionaries/zh/legal-terms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { LegalTermsDict } from "../types";

/**
* Chinese dictionary for `app/[locale]/legal/terms/page.tsx`. Only the page
* chrome is translated; the terms themselves are binding in English, which is
* what `updated` tells the reader.
*
* Spacing note: the space between `于` and `{date}` came from the literal
* space between two JSX expressions, and there is none before `。` because
* JSX dropped the newline-only whitespace that separated them. Do not "fix"
* either as a typo.
*/
export const legalTerms: LegalTermsDict = {
metaTitle: "服务条款 · Codewhale",
metaDescription: "Shannon Labs 产品 Codewhale 的服务条款。",
kicker: "法律",
title: "服务条款",
updated: "生效并最近更新于 {date}。以下为具有约束力的英文文本。",
privacyLink: "隐私政策",
homeLink: "返回首页",
};
2 changes: 1 addition & 1 deletion web/lib/i18n/iszh-ceiling.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const MIGRATION_HOME = join("lib", "i18n");
// One-way ceiling (#5519): the isZh migration converges or holds, never
// regresses. To lower the number: migrate branches into lib/i18n, then set
// CEILING to the new count reported by this test.
const CEILING = 18;
const CEILING = 15;

function walk(dir: string, out: string[] = []): string[] {
for (const entry of readdirSync(dir)) {
Expand Down
3 changes: 3 additions & 0 deletions web/scripts/check-locales.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,11 @@ const OPTIONAL_FILES = [
"docs-computers.ts",
"docs-auth.ts",
"docs-trust.ts",
"docs-work.ts",
"states.ts",
"changelog.ts",
"legal-terms.ts",
"legal-privacy.ts",
"computer-use.ts",
];

Expand Down
Loading