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
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,22 @@ jobs:
timeout_minutes: 10
command: bun run build

docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- uses: actions/setup-node@v4
with:
node-version: 22

- name: Install Mintlify CLI
run: npm install -g mintlify

- name: Validate docs
working-directory: docs
run: mintlify validate

test-e2e:
runs-on: ubuntu-latest
env:
Expand Down
99 changes: 99 additions & 0 deletions .github/workflows/translate-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
name: Translate Docs

on:
push:
branches: [main]
paths:
- 'docs/*.mdx'
- 'docs/**/*.mdx'
- 'README.md'
workflow_dispatch:

concurrency:
group: translate-docs

jobs:
translate:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write

env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_AUTH_TOKEN }}
ANTHROPIC_BASE_URL: ${{ secrets.ANTHROPIC_BASE_URL }}
FAILPROOFAI_TELEMETRY_DISABLED: "1"

steps:
- uses: actions/checkout@v6

- uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- uses: actions/cache@v5
with:
path: ~/.bun/install/cache
key: bun-${{ runner.os }}-${{ hashFiles('bun.lockb') }}
restore-keys: bun-${{ runner.os }}-

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Restore translation cache
uses: actions/cache/restore@v4
with:
path: scripts/translate-docs/.translation-cache.json
key: translation-cache-${{ hashFiles('scripts/translate-docs/.translation-cache.json') }}
restore-keys: translation-cache-

- name: Run translations
run: bun run translate --tier 3

- name: Save translation cache
uses: actions/cache/save@v4
with:
path: scripts/translate-docs/.translation-cache.json
key: translation-cache-${{ hashFiles('scripts/translate-docs/.translation-cache.json') }}

- name: Check for changes
id: changes
run: |
git add -A
if git diff --cached --quiet; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi

- name: Create PR if translations changed
if: steps.changes.outputs.changed == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Check for existing open translate PR
EXISTING=$(gh pr list --base main --search "[auto] update translations" --state open --json number --jq length)
if [ "$EXISTING" -gt 0 ]; then
echo "Translation PR already open. Skipping."
exit 0
fi

BRANCH="auto/translate-docs-$(date +%Y%m%d-%H%M)"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b "$BRANCH"
git commit -m "docs: update translations for changed English sources"
git push origin "$BRANCH"

PR_BODY="## Summary

Automated translation update triggered by changes to English documentation sources.

- Only changed pages were re-translated (content-hash cache)
- All 14 languages across 3 tiers"

gh pr create \
--title "[auto] update translations" \
--body "$PR_BODY" \
--base main \
--head "$BRANCH"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ next-env.d.ts
# package manager lockfiles (bun.lock is tracked; bun.lockb is binary)
package-lock.json

# translation cache
.translation-cache.json

# build artifacts from test scripts
*.tgz
/failproofai-local
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
### Features
- Add `changelog-check`, `docs-check`, and `pr-description-check` convention policies
- Track `.claude/settings.json` in git
- Add multilingual documentation with 14 languages and automated translation tooling (#93)
- Add GitHub Actions workflow to auto-translate docs when English sources change (#95)
- Add Mintlify docs validation to CI (#95)

### Fixes
- Accumulate all `instruct` messages instead of only delivering the first one
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
[![CI](https://img.shields.io/github/actions/workflow/status/exospherehost/failproofai/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/exospherehost/failproofai/actions)
[![Slack](https://img.shields.io/badge/Slack-join%20us-4A154B?style=flat-square&logo=slack)](https://join.slack.com/t/failproofai/shared_invite/zt-3v63b7k5e-O3NBHmj8X6n9gZSGDx6ggQ)

**Translations**: [简体中文](docs/i18n/README.zh.md) | [日本語](docs/i18n/README.ja.md) | [한국어](docs/i18n/README.ko.md) | [Español](docs/i18n/README.es.md) | [Português](docs/i18n/README.pt-br.md) | [Deutsch](docs/i18n/README.de.md) | [Français](docs/i18n/README.fr.md) | [Русский](docs/i18n/README.ru.md) | [हिन्दी](docs/i18n/README.hi.md) | [Türkçe](docs/i18n/README.tr.md) | [Tiếng Việt](docs/i18n/README.vi.md) | [Italiano](docs/i18n/README.it.md) | [العربية](docs/i18n/README.ar.md) | [עברית](docs/i18n/README.he.md)

The easiest way to manage policies that keep your AI agents reliable, on-task, and running autonomously - for **Claude Code** & the **Agents SDK**.

- **30 Built-in Policies** - Catch common agent failure modes out of the box. Block destructive commands, prevent secret leakage, keep agents inside project boundaries, detect loops, and more.
Expand Down Expand Up @@ -283,3 +285,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md).
## License

See [LICENSE](LICENSE).

---

Built and Maintained by **ExosphereHost: Reliability Research Lab for Your Agents**. We help enterprises and startups improve the reliability of their AI agents through our own agents, software, and expertise. Learn more at [exosphere.host](https://exosphere.host).
124 changes: 124 additions & 0 deletions __tests__/scripts/translate-docs/cache.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// @vitest-environment node
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import {
contentHash,
getCacheKey,
isCached,
setCacheEntry,
} from "@/scripts/translate-docs/cache";
import type { TranslationCache } from "@/scripts/translate-docs/types";

describe("contentHash", () => {
it("returns a 16-character hex string", () => {
const hash = contentHash("hello world");
expect(hash).toMatch(/^[0-9a-f]{16}$/);
});

it("returns the same hash for the same content", () => {
expect(contentHash("test")).toBe(contentHash("test"));
});

it("returns different hashes for different content", () => {
expect(contentHash("foo")).not.toBe(contentHash("bar"));
});
});

describe("getCacheKey", () => {
it("combines sourcePath and lang", () => {
expect(getCacheKey("introduction.mdx", "zh")).toBe("introduction.mdx::zh");
});

it("handles nested paths", () => {
expect(getCacheKey("cli/dashboard.mdx", "ja")).toBe(
"cli/dashboard.mdx::ja",
);
});
});

describe("isCached", () => {
it("returns false when entry does not exist", () => {
const cache: TranslationCache = {
sourceHash: "",
lastUpdated: "",
translations: {},
};
expect(isCached(cache, "intro.mdx", "zh", "content")).toBe(false);
});

it("returns true when source hash matches", () => {
const content = "some content";
const hash = contentHash(content);
const cache: TranslationCache = {
sourceHash: "",
lastUpdated: "",
translations: {
"intro.mdx::zh": {
sourceHash: hash,
targetLang: "zh",
translatedAt: "2024-01-01",
inputTokens: 100,
outputTokens: 200,
},
},
};
expect(isCached(cache, "intro.mdx", "zh", content)).toBe(true);
});

it("returns false when source hash does not match (content changed)", () => {
const cache: TranslationCache = {
sourceHash: "",
lastUpdated: "",
translations: {
"intro.mdx::zh": {
sourceHash: "oldhash123456789",
targetLang: "zh",
translatedAt: "2024-01-01",
inputTokens: 100,
outputTokens: 200,
},
},
};
expect(isCached(cache, "intro.mdx", "zh", "updated content")).toBe(false);
});
});

describe("setCacheEntry", () => {
it("creates a new entry in the cache", () => {
const cache: TranslationCache = {
sourceHash: "",
lastUpdated: "",
translations: {},
};
setCacheEntry(cache, "intro.mdx", "zh", "content", 100, 200);

const key = "intro.mdx::zh";
expect(cache.translations[key]).toBeDefined();
expect(cache.translations[key].targetLang).toBe("zh");
expect(cache.translations[key].sourceHash).toBe(contentHash("content"));
expect(cache.translations[key].inputTokens).toBe(100);
expect(cache.translations[key].outputTokens).toBe(200);
expect(cache.translations[key].translatedAt).toBeTruthy();
});

it("overwrites an existing entry", () => {
const cache: TranslationCache = {
sourceHash: "",
lastUpdated: "",
translations: {
"intro.mdx::zh": {
sourceHash: "old",
targetLang: "zh",
translatedAt: "2024-01-01",
inputTokens: 50,
outputTokens: 50,
},
},
};
setCacheEntry(cache, "intro.mdx", "zh", "new content", 300, 400);

const entry = cache.translations["intro.mdx::zh"];
expect(entry.sourceHash).toBe(contentHash("new content"));
expect(entry.inputTokens).toBe(300);
expect(entry.outputTokens).toBe(400);
});
});
98 changes: 98 additions & 0 deletions __tests__/scripts/translate-docs/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// @vitest-environment node
import { describe, it, expect } from "vitest";
import {
LANGUAGES,
getLanguagesByTier,
getLanguageByCode,
getLanguageCodes,
DO_NOT_TRANSLATE,
NAV_TRANSLATIONS,
} from "@/scripts/translate-docs/config";

describe("LANGUAGES", () => {
it("contains 14 languages", () => {
expect(LANGUAGES).toHaveLength(14);
});

it("has unique codes", () => {
const codes = LANGUAGES.map((l) => l.code);
expect(new Set(codes).size).toBe(codes.length);
});

it("assigns tiers 1-3", () => {
const tiers = new Set(LANGUAGES.map((l) => l.tier));
expect(tiers).toEqual(new Set([1, 2, 3]));
});

it("marks RTL languages", () => {
const rtl = LANGUAGES.filter((l) => l.rtl);
expect(rtl.map((l) => l.code).sort()).toEqual(["ar", "he"]);
});
});

describe("getLanguagesByTier", () => {
it("returns 7 tier-1 languages", () => {
expect(getLanguagesByTier(1)).toHaveLength(7);
});

it("returns 12 tier-1+2 languages", () => {
expect(getLanguagesByTier(2)).toHaveLength(12);
});

it("returns all 14 for tier 3", () => {
expect(getLanguagesByTier(3)).toHaveLength(14);
});
});

describe("getLanguageByCode", () => {
it("finds a known language", () => {
const ja = getLanguageByCode("ja");
expect(ja).toBeDefined();
expect(ja!.name).toBe("Japanese");
expect(ja!.nativeName).toBe("日本語");
});

it("returns undefined for unknown code", () => {
expect(getLanguageByCode("xx")).toBeUndefined();
});
});

describe("getLanguageCodes", () => {
it("returns all codes when no tier specified", () => {
expect(getLanguageCodes()).toHaveLength(14);
});

it("filters by tier", () => {
expect(getLanguageCodes(1)).toHaveLength(7);
});
});

describe("DO_NOT_TRANSLATE", () => {
it("includes key product names", () => {
expect(DO_NOT_TRANSLATE).toContain("failproofai");
expect(DO_NOT_TRANSLATE).toContain("Claude Code");
expect(DO_NOT_TRANSLATE).toContain("Agents SDK");
});

it("includes CLI flags", () => {
expect(DO_NOT_TRANSLATE).toContain("--install");
expect(DO_NOT_TRANSLATE).toContain("--uninstall");
});

it("includes policy names", () => {
expect(DO_NOT_TRANSLATE).toContain("block-sudo");
expect(DO_NOT_TRANSLATE).toContain("sanitize-api-keys");
});
});

describe("NAV_TRANSLATIONS", () => {
it("has entries for all 14 languages plus English", () => {
const expectedCodes = ["en", ...LANGUAGES.map((l) => l.code)];
for (const code of expectedCodes) {
expect(NAV_TRANSLATIONS[code]).toBeDefined();
expect(NAV_TRANSLATIONS[code].docs).toBeTruthy();
expect(NAV_TRANSLATIONS[code].gettingStarted).toBeTruthy();
expect(NAV_TRANSLATIONS[code].cli).toBe("CLI"); // CLI stays as-is in all languages
}
});
});
Loading
Loading