Skip to content
Open
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
File renamed without changes.
File renamed without changes.
15 changes: 15 additions & 0 deletions app/academy/japan/[[...slug]]/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Link from "next/link";

export default function JaAcademyNotFound() {
return (
<div className="flex flex-col items-center justify-center gap-4 py-16 text-center">
<h1 className="text-2xl font-semibold">ページが見つかりません</h1>
<p className="text-fd-muted-foreground">
お探しのページは存在しないか、移動されました。
</p>
<Link href="/academy/japan" className="text-fd-primary hover:underline">
Academy トップへ
</Link>
</div>
);
}
48 changes: 48 additions & 0 deletions app/academy/japan/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { academyJaSource } from "@/lib/source";
import { DocsChromePage } from "@/components/DocsChromePage";
import { buildSectionMetadata } from "@/lib/mdx-page";

type PageProps = {
params: Promise<{ slug?: string[] }>;
};

export default async function JaAcademyPage({ params }: PageProps) {
const { slug = [] } = await params;
const page = academyJaSource.getPage(slug);
if (!page) notFound();
return (
<DocsChromePage
page={page}
bottomSuffix={
<div className="mt-10 text-right text-xs italic text-fd-muted-foreground">
Translation by{" "}
<a
href="https://gao-ai.com"
target="_blank"
rel="noopener noreferrer"
className="underline underline-offset-2 decoration-1 hover:no-underline"
>
GAO, Inc.
</a>
</div>
}
/>
);
}

export async function generateMetadata({
params,
}: PageProps): Promise<Metadata> {
const { slug = [] } = await params;
const page = academyJaSource.getPage(slug);
if (!page) return { title: "Not Found" };
return buildSectionMetadata(page, "academy/japan", "Academy", slug);
}

export function generateStaticParams() {
return academyJaSource
.generateParams()
.map((p) => (p.slug.length > 0 ? { slug: p.slug } : {}));
}
14 changes: 14 additions & 0 deletions app/academy/japan/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { academyJaSource } from "@/lib/source";
import { SharedDocsLayout } from "@/components/layout";

export default function JaAcademyLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<SharedDocsLayout tree={academyJaSource.getPageTree()}>
{children}
</SharedDocsLayout>
);
}
6 changes: 5 additions & 1 deletion components/DocsChromePage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import "server-only";
import type { ComponentProps, ComponentType } from "react";
import type { ComponentProps, ComponentType, ReactNode } from "react";
import { DocsPage } from "fumadocs-ui/page";
import type { TOCItemType } from "fumadocs-core/toc";

Expand Down Expand Up @@ -32,10 +32,13 @@ const getIsoDate = (value: unknown): string | undefined => {
export async function DocsChromePage({
page,
bodyChromeProps,
bottomSuffix,
}: {
page: LoadedPage;
/** Extra props forwarded to `DocBodyChrome` (e.g. `versionLabel` on self-hosting). */
bodyChromeProps?: BodyChromeProps;
/** Optional node rendered inside DocBodyChrome, after the MDX body. */
bottomSuffix?: ReactNode;
}) {
const data = page.data;
const loaded =
Expand Down Expand Up @@ -63,6 +66,7 @@ export async function DocsChromePage({
>
<DocBodyChrome {...bodyChromeProps}>
<MDX components={getMDXComponents()} />
{bottomSuffix}
</DocBodyChrome>
</DocsPage>
);
Expand Down
11 changes: 8 additions & 3 deletions components/academy/AgentPromptCallout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,20 @@ export interface AgentPromptCalloutProps {
lede?: React.ReactNode;
/** The exact text written to the clipboard. */
prompt: string;
locale?: string;
}

export function AgentPromptCallout({
ribbon = "Run with your agent",
ribbon,
title,
lede,
prompt,
locale,
}: AgentPromptCalloutProps) {
const [copied, setCopied] = useState(false);
const effectiveRibbon =
ribbon ??
(locale === "ja" ? "エージェントで実行する" : "Run with your agent");

const onCopy = () => {
if (typeof navigator === "undefined" || !navigator.clipboard) return;
Expand All @@ -36,7 +41,7 @@ export function AgentPromptCallout({
<figure
className="agent-prompt not-prose corner-box-corners"
role="region"
aria-label={ribbon}
aria-label={effectiveRibbon}
>
<header className="agent-prompt__ribbon">
<span className="agent-prompt__mark" aria-hidden="true">
Expand All @@ -50,7 +55,7 @@ export function AgentPromptCallout({
/>
</svg>
</span>
<span>{ribbon}</span>
<span>{effectiveRibbon}</span>
</header>

{(title || lede) && (
Expand Down
37 changes: 25 additions & 12 deletions components/academy/DatasetFieldsDiagram.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ function Node({
);
}

export function DatasetFieldsDiagram() {
export function DatasetFieldsDiagram({ locale }: { locale?: string } = {}) {
const isJa = locale === "ja";
const wrapRef = useRef<HTMLDivElement>(null);
const innerRef = useRef<HTMLDivElement>(null);
const estScale = estimateInitialScale();
Expand Down Expand Up @@ -86,7 +87,11 @@ export function DatasetFieldsDiagram() {
return (
<figure
className="dataset-fields not-prose"
aria-label="Dataset fields and when they are used"
aria-label={
locale === "ja"
? "データセットフィールドとその用途"
: "Dataset fields and when they are used"
}
>
<div
ref={wrapRef}
Expand All @@ -105,9 +110,17 @@ export function DatasetFieldsDiagram() {
<div className="dataset-fields__row-divider" style={{ top: 128 }} />
<div className="dataset-fields__row-divider" style={{ top: 257 }} />

<StageLabel y={0} num="01" name="Run experiment" />
<StageLabel y={128} num="02" name="Add context" />
<StageLabel y={257} num="03" name="Score" />
<StageLabel
y={0}
num="01"
name={isJa ? "実験を実行" : "Run experiment"}
/>
<StageLabel
y={128}
num="02"
name={isJa ? "コンテキストを追加" : "Add context"}
/>
<StageLabel y={257} num="03" name={isJa ? "スコアリング" : "Score"} />

<svg
className="dataset-fields__arrows"
Expand Down Expand Up @@ -171,27 +184,27 @@ export function DatasetFieldsDiagram() {
</svg>

<Node x={160} y={36} width={180} variant="live">
Input
{isJa ? "入力" : "Input"}
</Node>
<Node x={400} y={36} width={220} variant="live">
Task execution
{isJa ? "タスク実行" : "Task execution"}
</Node>
<Node x={680} y={36} width={180} variant="live">
Actual output
{isJa ? "実出力" : "Actual output"}
</Node>

<Node x={160} y={164} width={180} variant="stripe">
Metadata
{isJa ? "メタデータ" : "Metadata"}
</Node>
<Node x={400} y={164} width={220} variant="stripe">
Extra context for review
{isJa ? "レビュー用の追加コンテキスト" : "Extra context for review"}
</Node>

<Node x={160} y={293} width={180}>
Expected output
{isJa ? "期待出力" : "Expected output"}
</Node>
<Node x={680} y={293} width={180} variant="terminal">
Score / compare
{isJa ? "スコア / 比較" : "Score / compare"}
</Node>
</div>
</div>
Expand Down
21 changes: 18 additions & 3 deletions components/academy/ErrorAnalysisProcessDiagram.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,27 @@
},
];

const STEPS_JA = [
{ num: "01", label: "収集", title: "トレースを集める" },
{ num: "02", label: "記録", title: "Open coding" },
{ num: "03", label: "グループ化", title: "カテゴリにクラスタリング" },
{ num: "04", label: "定量化", title: "ラベル付けと計測" },
{ num: "05", label: "実行", title: "判断と実行", accent: true },
];

function estimateInitialScale(): number {
if (typeof window === "undefined") return 0.65;
const vw = document.documentElement.clientWidth;
return Math.min(1, Math.max(0.3, (vw - 32) / INNER_W));
}

export function ErrorAnalysisProcessDiagram() {
export function ErrorAnalysisProcessDiagram({
locale,
}: { locale?: string } = {}) {
const wrapRef = useRef<HTMLDivElement>(null);
const innerRef = useRef<HTMLDivElement>(null);
const estScale = estimateInitialScale();
const steps = locale === "ja" ? STEPS_JA : STEPS;

useLayoutEffect(() => {
const wrap = wrapRef.current;
Expand Down Expand Up @@ -73,7 +84,11 @@
return (
<figure
className="error-analysis-process not-prose"
aria-label="The five steps of the error analysis process"
aria-label={
locale === "ja"
? "エラー分析プロセスの5つのステップ"
: "The five steps of the error analysis process"
}
>
<div
ref={wrapRef}
Expand All @@ -87,7 +102,7 @@
className="error-analysis-process__canvas"
style={{ transform: `scale(${estScale})` }}
>
{STEPS.map((step, i) => (
{steps.map((step, i) => (

Check warning on line 105 in components/academy/ErrorAnalysisProcessDiagram.tsx

View check run for this annotation

Claude / Claude Code Review

Use steps.length, not STEPS.length, for connector check

The .map at line 105 iterates the locale-aware `steps` array, but the connector guard at line 120 still references `STEPS.length` (the English-only constant). Both `STEPS` and `STEPS_JA` happen to have 5 entries today, so there is no user-visible defect, but a future contributor adding or removing a step from one array only would render an orphan connector or miss one. One-line fix: `STEPS.length` → `steps.length`.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The .map at line 105 iterates the locale-aware steps array, but the connector guard at line 120 still references STEPS.length (the English-only constant). Both STEPS and STEPS_JA happen to have 5 entries today, so there is no user-visible defect, but a future contributor adding or removing a step from one array only would render an orphan connector or miss one. One-line fix: STEPS.lengthsteps.length.

Extended reasoning...

What the bug is. This PR introduces a locale-aware steps array (const steps = locale === "ja" ? STEPS_JA : STEPS) and correctly switches the .map call at line 105 from STEPS.map to steps.map. However, the connector guard immediately below at line 120 still reads {i < STEPS.length - 1 && ...}, referencing the English-only STEPS constant rather than the locale-selected steps array.

Why it's latent today. STEPS and STEPS_JA each contain exactly 5 entries, so STEPS.length === steps.length for every supported locale and the rendered output is identical to using steps.length. No user will see a visual defect on this PR as it stands — this is purely a code-quality / consistency issue.

Why it still matters. The diff introduced steps precisely to abstract over locale, and one of the two sites that needs it was missed. The inconsistency is a footgun for future contributors: if someone adds or removes a step from STEPS_JA (or adds a third locale array, e.g. STEPS_DE) and forgets to make a matching edit to STEPS, the connector count will no longer match the rendered step count — producing either an orphan trailing connector or a missing connector between the last two cards.

Step-by-step proof. Suppose a future contributor adds a 6th step to STEPS_JA:

  1. steps = STEPS_JA now has length 6.
  2. The .map at line 105 renders 6 step cards (indices i=0..5).
  3. The guard at line 120 evaluates i < STEPS.length - 1, i.e. i < 4 (because STEPS.length is still 5).
  4. Connectors are rendered for i=0,1,2,3 — only 4 connectors between 6 cards.
  5. The 5th and 6th cards have no connector between them. Conversely, if a step is removed from STEPS while STEPS_JA keeps 5, an extra connector renders past the last card.

How to fix. Change STEPS.length to steps.length on line 120 so the guard derives from the same array being iterated. This is the same one-line change Greptile-style reviewers would call out — defensive, consistent with the abstraction the diff introduced.

<div key={step.num} className="error-analysis-process__row-cell">
<article
className={`error-analysis-process__step corner-box-corners${
Expand Down
20 changes: 15 additions & 5 deletions components/academy/EvaluationEvolutionDiagram.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -294,34 +294,44 @@ function Stage({
);
}

export function EvaluationEvolutionDiagram() {
export function EvaluationEvolutionDiagram({
locale,
}: { locale?: string } = {}) {
return (
<figure
className="evaluation-evolution not-prose"
aria-label="How evaluation typically evolves from manual review to automated evaluators"
aria-label={
locale === "ja"
? "評価が手動レビューから自動評価器へと進化する流れ"
: "How evaluation typically evolves from manual review to automated evaluators"
}
>
<div className="evaluation-evolution__wrap">
<div className="evaluation-evolution__flow">
<Stage
className="evaluation-evolution__stage--one"
step="Step 01"
title="Manual review"
title={locale === "ja" ? "手動レビュー" : "Manual review"}
>
<ManualReviewGlyph />
</Stage>
<Connector />
<Stage
className="evaluation-evolution__stage--two"
step="Step 02"
title={"Identify\nfailure\u00a0modes"}
title={
locale === "ja"
? "\u5931\u6557\u30e2\u30fc\u30c9\u3092\n\u7279\u5b9a\u3059\u308b"
: "Identify\nfailure\u00a0modes"
}
>
<FailureModesGlyph />
</Stage>
<Connector />
<Stage
className="evaluation-evolution__stage--three"
step="Step 03"
title="Automate"
title={locale === "ja" ? "自動化" : "Automate"}
>
<AutomateGlyph />
</Stage>
Expand Down
Loading
Loading