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
9 changes: 9 additions & 0 deletions .github/workflows/translate-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,15 @@ jobs:
merge-multiple: true
path: docs

# The per-language jobs prune their own tree, but this job re-checks out
# main and *overlays* the artifacts — download-artifact only adds files,
# it never deletes — so any orphan committed on main is resurrected here.
# Prune again on the merged tree, which is what actually gets committed.
- name: Prune translations whose English source was deleted
run: >-
bun scripts/translate-docs/cli.ts --prune
--languages zh,ja,ko,es,pt-br,de,fr,ru,hi,tr,vi,it,ar,he

- name: Update localized product navigation
run: >-
bun scripts/translate-docs/cli.ts --update-nav
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@

## 0.0.14-beta.1 — 2026-07-17

### Docs
- Point the docs navbar button at the landing page's "talk to us" booking link instead of `/getting-started`, so the primary CTA matches befailproof.ai. (#556)
- Give the docs footer a way back to the product: add `website` + `discord` to the footer socials and Product (Home / Blog / Guides) and Resources (npm / GitHub / Discord) link columns, mirroring the befailproof.ai footer. Previously the docs linked back to the marketing site from nowhere. (#556)
- Redirect the 13 AgentEye pages the upstream syncs deleted (`/agenteye/deployment`, `/agenteye/kubernetes-deployment`, `/agenteye/troubleshooting`, …) to a live page instead of hard-404ing. (#556)

### Fixes
- Delete translated pages whose English source no longer exists, and stop them coming back: `translate-docs` gained a `--prune` mode that runs by default on every translation pass (`--no-prune` opts out) and as an explicit step in the `consolidate` job — that job re-checks-out `main` and *overlays* the artifacts, so a prune done only in the per-language jobs would be undone. Translation only ever moved forward, so the 11 pages the AgentEye syncs removed upstream left 154 orphans (11 × 14 locales) that `--update-nav` dropped from the sidebar but Mintlify still served and indexed — non-English readers could land on docs for a deleted feature with no way out. A repo invariant test now fails if any translation outlives its English source. (#556)
- Move the docs auto-translation daily cron from 06:00 UTC to 11:05 AM IST (05:35 UTC, encoded as `35 5 * * *` since GitHub Actions cron is always UTC). (#553)

## 0.0.14-beta.1 — 2026-07-14
Expand Down
148 changes: 147 additions & 1 deletion __tests__/scripts/translate-docs/mdx-translator.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
// @vitest-environment node
import { describe, it, expect } from "vitest";
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { mkdtempSync, mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
rewriteInternalLinks,
sanitizeJsxAttributes,
stripStrayTrailingFence,
convertHtmlComments,
getEnglishMdxPages,
pruneOrphanedTranslations,
} from "@/scripts/translate-docs/mdx-translator";
import type { TranslationCache } from "@/scripts/translate-docs/types";

describe("getEnglishMdxPages", () => {
it("includes AgentEye pages in automatic translation", () => {
Expand All @@ -16,6 +21,147 @@ describe("getEnglishMdxPages", () => {
});
});

describe("pruneOrphanedTranslations", () => {
let docsDir: string;

/** Build a fixture docs tree; `page` paths are relative to the docs root. */
function write(page: string, body = "# hi\n") {
const full = join(docsDir, page);
mkdirSync(join(full, ".."), { recursive: true });
writeFileSync(full, body);
return full;
}

beforeEach(() => {
docsDir = mkdtempSync(join(tmpdir(), "prune-docs-"));
});

afterEach(() => {
rmSync(docsDir, { recursive: true, force: true });
});

it("removes a translation whose English source is gone", () => {
const orphan = write("zh/agenteye/deployment.mdx");

const removed = pruneOrphanedTranslations(["zh"], { docsDir });

expect(removed).toEqual(["zh/agenteye/deployment.mdx"]);
expect(existsSync(orphan)).toBe(false);
});

it("keeps a translation whose English source still exists", () => {
write("agenteye/overview.mdx");
const live = write("zh/agenteye/overview.mdx");

const removed = pruneOrphanedTranslations(["zh"], { docsDir });

expect(removed).toEqual([]);
expect(existsSync(live)).toBe(true);
});

it("reports without deleting under dryRun", () => {
const orphan = write("zh/agenteye/deployment.mdx");

const removed = pruneOrphanedTranslations(["zh"], { docsDir, dryRun: true });

expect(removed).toEqual(["zh/agenteye/deployment.mdx"]);
expect(existsSync(orphan)).toBe(true);
});

it("drops the cache entry so a re-added page is not skipped as cached", () => {
write("zh/agenteye/deployment.mdx");
const cache: TranslationCache = {
sourceHash: "",
lastUpdated: "",
translations: {
"agenteye/deployment.mdx::zh": {
sourceHash: "abc123",
targetLang: "zh",
translatedAt: "2026-01-01T00:00:00.000Z",
inputTokens: 1,
outputTokens: 1,
},
"agenteye/overview.mdx::zh": {
sourceHash: "def456",
targetLang: "zh",
translatedAt: "2026-01-01T00:00:00.000Z",
inputTokens: 1,
outputTokens: 1,
},
},
};

pruneOrphanedTranslations(["zh"], { docsDir, cache });

expect(cache.translations).not.toHaveProperty("agenteye/deployment.mdx::zh");
expect(cache.translations).toHaveProperty("agenteye/overview.mdx::zh");
});

it("leaves the cache untouched under dryRun", () => {
write("zh/agenteye/deployment.mdx");
const cache: TranslationCache = {
sourceHash: "",
lastUpdated: "",
translations: {
"agenteye/deployment.mdx::zh": {
sourceHash: "abc123",
targetLang: "zh",
translatedAt: "2026-01-01T00:00:00.000Z",
inputTokens: 1,
outputTokens: 1,
},
},
};

pruneOrphanedTranslations(["zh"], { docsDir, cache, dryRun: true });

expect(cache.translations).toHaveProperty("agenteye/deployment.mdx::zh");
});

it("only touches the languages it is given", () => {
const zh = write("zh/agenteye/deployment.mdx");
const ja = write("ja/agenteye/deployment.mdx");

const removed = pruneOrphanedTranslations(["zh"], { docsDir });

expect(removed).toEqual(["zh/agenteye/deployment.mdx"]);
expect(existsSync(zh)).toBe(false);
expect(existsSync(ja)).toBe(true);
});

it("skips a language with no directory on disk", () => {
expect(() =>
pruneOrphanedTranslations(["pt-br"], { docsDir }),
).not.toThrow();
expect(pruneOrphanedTranslations(["pt-br"], { docsDir })).toEqual([]);
});

it("prunes nested non-agenteye pages too", () => {
write("cli/hook.mdx");
const liveCli = write("zh/cli/hook.mdx");
const orphanCli = write("zh/cli/removed-command.mdx");

const removed = pruneOrphanedTranslations(["zh"], { docsDir });

expect(removed).toEqual(["zh/cli/removed-command.mdx"]);
expect(existsSync(liveCli)).toBe(true);
expect(existsSync(orphanCli)).toBe(false);
});
});

describe("translation tree invariant", () => {
// Guards the real docs/ tree: an English page deleted upstream (the agenteye
// sync does this routinely) must not leave its 14 translations behind, live
// and indexable, documenting a feature that no longer exists.
it("has no translated page whose English source is missing", () => {
const orphans = pruneOrphanedTranslations(
["zh", "ja", "ko", "es", "pt-br", "de", "fr", "ru", "hi", "tr", "vi", "it", "ar", "he"],
{ dryRun: true },
);
expect(orphans).toEqual([]);
});
});

describe("rewriteInternalLinks", () => {
it("rewrites MDX component href attributes with language prefix", () => {
const input = `<Card title="Policies" href="/built-in-policies">`;
Expand Down
36 changes: 36 additions & 0 deletions __tests__/scripts/translate-docs/mintlify-nav.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,48 @@
// @vitest-environment node
import { describe, it, expect } from "vitest";
import { existsSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import {
buildLanguageNav,
generateLanguagesArray,
getNavigationPageReferences,
localizeProductsNavigation,
readDocsConfig,
} from "@/scripts/translate-docs/mintlify-nav";

const DOCS_DIR = join(
dirname(fileURLToPath(import.meta.url)),
"..",
"..",
"..",
"docs",
);

describe("docs.json redirects", () => {
interface Redirect {
source: string;
destination: string;
}
const redirects = (readDocsConfig().redirects ?? []) as Redirect[];

it("points every redirect at a page that exists", () => {
const broken = redirects.filter(
(r) => !existsSync(join(DOCS_DIR, `${r.destination}.mdx`)),
);
expect(broken).toEqual([]);
});

it("never shadows a live page with a redirect", () => {
// A redirect whose source still resolves to a real .mdx would make that
// page permanently unreachable.
const shadowing = redirects.filter((r) =>
existsSync(join(DOCS_DIR, `${r.source}.mdx`)),
);
expect(shadowing).toEqual([]);
});
});

const sampleEnglishTabs = [
{
tab: "Docs",
Expand Down
Loading