failproofai 0.0.14-beta.0 → 0.0.14-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.codex/hooks.json +6 -6
- package/.next/standalone/.cursor/hooks.json +6 -6
- package/.next/standalone/.factory/hooks.json +9 -9
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +5 -5
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0--lkk6._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-219ec._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__04_h-a5._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__06cuf1y._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08w4wmd._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g6bbuw._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jnjf2t._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q-v9z2._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q0qzx1._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0q2nbsl._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0rv7m0k._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xuaoik._.js +3 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__16le-kd._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__17g9wh7._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ffkmds._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1legmza._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1mrihkj._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qb590j._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qxztj-._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1shcqgr._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1xuedea._.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +4 -4
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_01r25oi._.js +1 -1
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00cz9as._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__015_i4t._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__030k0c6._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__03lv-pe._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__042d3cd._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__05r_17v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09q1168._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__07qfsfa._.js → [root-of-the-server]__09v9cwb._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e2l3c1._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h12me5._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hci7t3._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hdsupo._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p84xee._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qeuy9c._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s89hoe._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0twi4fj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ynf7tx._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11rtg6s._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__14dd6h8._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__170799-._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17ai7sy._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1dky4g0._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1nq7ivq._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1tkjqka._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__212nf49._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_05whahf._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1kje4fm._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_087bt9w._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_1fvisnp._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_209u41o._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_0xkhw_v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_0g5r64e.js → node_modules_html-to-image_es_index_06xsmte.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0-uvagv.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_03c7gi5.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_09p-8om.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0v-kfiu.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_0xrgzyz.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_1806lsc.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_1j6dd-e.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_esm_build_templates_app-page_1sa65r-.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_posthog-node_dist_entrypoints_index_node_mjs_11bnuzn._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1ezd2jf._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1tnuifj._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +10 -10
- package/.next/standalone/.next/static/chunks/{13f1kmjea-0md.js → 02jcotghcxrbu.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1374x9bv_s_vg.js → 0h-6ymo9v86b0.js} +2 -2
- package/.next/standalone/.next/static/chunks/0pndjhcxfu-0t.js +1 -0
- package/.next/standalone/.next/static/chunks/1-33f82_l32or.js +1 -0
- package/.next/standalone/.next/static/chunks/{003uz088wbmz7.js → 1bg_g3g359c_d.js} +1 -1
- package/.next/standalone/.next/static/chunks/{454dyizqik6t9.js → 1gg-8e9q_m__9.js} +2 -2
- package/.next/standalone/.next/static/chunks/{2ufuwhv03yvtb.js → 29zkom4s9de2d.js} +1 -1
- package/.next/standalone/.next/static/chunks/{17sdztgbcc0fi.js → 2llkdafnaejw_.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0iinhn4417nk1.js → 3-ogr79xqbw0h.js} +1 -1
- package/.next/standalone/.next/static/chunks/36j05kb2x323i.css +2 -0
- package/.next/standalone/.next/static/chunks/{43weu8f59tlix.js → 388hgfqye67xa.js} +1 -1
- package/.next/standalone/.next/static/chunks/396764jgd7-9j.js +1 -0
- package/.next/standalone/.next/static/chunks/3w7g0n0iwwnbs.js +1 -0
- package/.next/standalone/.next/static/chunks/{turbopack-0vavqo2d-tlt3.js → turbopack-10hj8t5pf33tu.js} +1 -1
- package/.next/standalone/.opencode/plugins/failproofai.mjs +30 -11
- package/.next/standalone/app/audit/_components/how-to-improve-section.tsx +11 -2
- package/.next/standalone/app/components/copy-button.tsx +22 -6
- package/.next/standalone/app/components/global-error-listeners.tsx +27 -0
- package/.next/standalone/components/reach-developers.tsx +1 -1
- package/.next/standalone/docker-hook-sync/Dockerfile +8 -4
- package/.next/standalone/integration-suite/Dockerfile +47 -0
- package/.next/standalone/integration-suite/README.md +87 -0
- package/.next/standalone/integration-suite/canary-policies.mjs +45 -0
- package/.next/standalone/integration-suite/capture-tokens.sh +48 -0
- package/.next/standalone/integration-suite/ci-entrypoint.sh +164 -0
- package/.next/standalone/integration-suite/inject-tokens.sh +44 -0
- package/.next/standalone/integration-suite/install-clis.sh +91 -0
- package/.next/standalone/integration-suite/probe-cli.sh +208 -0
- package/.next/standalone/integration-suite/report.js +74 -0
- package/.next/standalone/integration-suite/run.sh +107 -0
- package/.next/standalone/lib/error-origin.ts +67 -0
- package/.next/standalone/lib/format-duration.ts +23 -9
- package/.next/standalone/lib/install-check.ts +231 -0
- package/.next/standalone/package.json +1 -3
- package/.next/standalone/server.js +1 -1
- package/README.md +1 -17
- package/bin/failproofai.mjs +11 -1
- package/dist/cli.mjs +818 -320
- package/lib/error-origin.ts +67 -0
- package/lib/format-duration.ts +23 -9
- package/lib/install-check.ts +231 -0
- package/package.json +1 -3
- package/scripts/dev-hook.mjs +292 -0
- package/scripts/install-diagnosis.mjs +5 -8
- package/scripts/install-telemetry.mjs +12 -4
- package/scripts/translate-docs/cli.ts +53 -1
- package/scripts/translate-docs/mdx-translator.ts +158 -18
- package/scripts/translate-docs/readme-translator.ts +36 -18
- package/scripts/translate-docs/translator.ts +180 -9
- package/scripts/translate-docs/types.ts +6 -0
- package/scripts/translate-docs/validate-translation.ts +132 -0
- package/scripts/validate-mdx.ts +92 -15
- package/src/audit/cli.ts +53 -15
- package/src/auth/cli.ts +14 -5
- package/src/hooks/builtin-policies.ts +168 -50
- package/src/hooks/configure-wizard.ts +238 -22
- package/src/hooks/custom-hooks-loader.ts +70 -4
- package/src/hooks/handler.ts +4 -1
- package/src/hooks/policy-types.ts +8 -0
- package/src/hooks/tui.ts +96 -29
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ge8e7w._.js +0 -3
- package/.next/standalone/.next/static/chunks/0405htbxay9vd.js +0 -1
- package/.next/standalone/.next/static/chunks/2m_tbl39fcedn.js +0 -1
- package/.next/standalone/.next/static/chunks/2o3ql0n5owqsw.js +0 -1
- package/.next/standalone/.next/static/chunks/35-90m9py64d4.js +0 -1
- package/.next/standalone/.next/static/chunks/3r--yyjkp9_7b.css +0 -2
- package/scripts/postinstall.mjs +0 -242
- package/scripts/preuninstall.mjs +0 -131
- /package/.next/standalone/.next/static/{MNJWFb2jLRrxRKGFNK6Hy → YKxI22hC-F_nHp7MEKfb1}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{MNJWFb2jLRrxRKGFNK6Hy → YKxI22hC-F_nHp7MEKfb1}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{MNJWFb2jLRrxRKGFNK6Hy → YKxI22hC-F_nHp7MEKfb1}/_ssgManifest.js +0 -0
|
@@ -80,12 +80,20 @@ function getInstanceId() {
|
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
82
|
* Track a named event to PostHog. No-op when telemetry is disabled.
|
|
83
|
-
*
|
|
83
|
+
*
|
|
84
|
+
* `opts.version` must be supplied by callers outside an npm lifecycle script:
|
|
85
|
+
* npm only sets npm_package_version while running one, and these events now
|
|
86
|
+
* fire from the CLI (see lib/install-check.ts) because package managers block
|
|
87
|
+
* install scripts by default.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} event
|
|
90
|
+
* @param {Record<string, unknown>} [properties]
|
|
91
|
+
* @param {{ version?: string, timeoutMs?: number }} [opts]
|
|
84
92
|
*/
|
|
85
|
-
export async function trackInstallEvent(event, properties = {}) {
|
|
93
|
+
export async function trackInstallEvent(event, properties = {}, opts = {}) {
|
|
86
94
|
if (process.env.FAILPROOFAI_TELEMETRY_DISABLED === "1") return;
|
|
87
95
|
|
|
88
|
-
const version = process.env.npm_package_version ?? "unknown";
|
|
96
|
+
const version = opts.version ?? process.env.npm_package_version ?? "unknown";
|
|
89
97
|
const body = JSON.stringify({
|
|
90
98
|
api_key: process.env.FAILPROOFAI_POSTHOG_KEY ?? API_KEY,
|
|
91
99
|
event,
|
|
@@ -106,7 +114,7 @@ export async function trackInstallEvent(event, properties = {}) {
|
|
|
106
114
|
method: "POST",
|
|
107
115
|
headers: { "Content-Type": "application/json" },
|
|
108
116
|
body,
|
|
109
|
-
signal: AbortSignal.timeout(5000),
|
|
117
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? 5000),
|
|
110
118
|
}
|
|
111
119
|
);
|
|
112
120
|
}
|
|
@@ -9,7 +9,11 @@ import {
|
|
|
9
9
|
getLanguageByCode,
|
|
10
10
|
getModelForTier,
|
|
11
11
|
} from "./config";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
getEnglishMdxPages,
|
|
14
|
+
translateMdxPage,
|
|
15
|
+
pruneOrphanedTranslations,
|
|
16
|
+
} from "./mdx-translator";
|
|
13
17
|
import { translateReadme } from "./readme-translator";
|
|
14
18
|
import {
|
|
15
19
|
getNavigationPageReferences,
|
|
@@ -33,6 +37,8 @@ const { values: args } = parseArgs({
|
|
|
33
37
|
force: { type: "boolean", short: "f", default: false },
|
|
34
38
|
"update-nav": { type: "boolean", default: false },
|
|
35
39
|
validate: { type: "boolean", default: false },
|
|
40
|
+
prune: { type: "boolean", default: false },
|
|
41
|
+
"no-prune": { type: "boolean", default: false },
|
|
36
42
|
model: { type: "string", short: "m" },
|
|
37
43
|
help: { type: "boolean", short: "h", default: false },
|
|
38
44
|
},
|
|
@@ -53,6 +59,8 @@ Options:
|
|
|
53
59
|
-f, --force Ignore cache, re-translate everything
|
|
54
60
|
--update-nav Regenerate docs.json navigation after translation
|
|
55
61
|
--validate Check all nav references resolve to files
|
|
62
|
+
--prune Only delete translations whose English source is gone
|
|
63
|
+
--no-prune Skip the prune step during a normal translation run
|
|
56
64
|
-m, --model <model> Claude model override (default: Sonnet for Tier 1, Haiku for Tier 2/3)
|
|
57
65
|
-h, --help Show this help
|
|
58
66
|
|
|
@@ -65,6 +73,7 @@ Examples:
|
|
|
65
73
|
bun scripts/translate-docs/cli.ts --dry-run --tier 3 # Preview all translations
|
|
66
74
|
bun scripts/translate-docs/cli.ts --validate # Check nav references
|
|
67
75
|
bun scripts/translate-docs/cli.ts --update-nav # Regenerate docs.json
|
|
76
|
+
bun scripts/translate-docs/cli.ts --prune --tier 3 # Drop orphaned translations
|
|
68
77
|
`);
|
|
69
78
|
process.exit(0);
|
|
70
79
|
}
|
|
@@ -132,6 +141,26 @@ async function main() {
|
|
|
132
141
|
return;
|
|
133
142
|
}
|
|
134
143
|
|
|
144
|
+
// Prune-only mode
|
|
145
|
+
if (args.prune) {
|
|
146
|
+
const langCodes = resolveLanguages();
|
|
147
|
+
const cache = readCache();
|
|
148
|
+
const pruned = pruneOrphanedTranslations(langCodes, {
|
|
149
|
+
dryRun: args["dry-run"],
|
|
150
|
+
cache,
|
|
151
|
+
});
|
|
152
|
+
for (const file of pruned) {
|
|
153
|
+
console.log(
|
|
154
|
+
` ${args["dry-run"] ? "would remove" : "removed"}: ${file}`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (pruned.length > 0 && !args["dry-run"]) writeCache(cache);
|
|
158
|
+
console.log(
|
|
159
|
+
`\n${pruned.length} orphaned translation(s)${args["dry-run"] ? " would be" : ""} removed.`,
|
|
160
|
+
);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
135
164
|
const langCodes = resolveLanguages();
|
|
136
165
|
const isDryRun = args["dry-run"];
|
|
137
166
|
const isForce = args.force;
|
|
@@ -196,6 +225,23 @@ async function main() {
|
|
|
196
225
|
|
|
197
226
|
// Translate docs
|
|
198
227
|
if (!args["readme-only"]) {
|
|
228
|
+
// Drop translations orphaned by an upstream English deletion before
|
|
229
|
+
// translating, so a stale locale page can never outlive its source.
|
|
230
|
+
if (!args["no-prune"]) {
|
|
231
|
+
const pruned = pruneOrphanedTranslations(langCodes, {
|
|
232
|
+
dryRun: isDryRun,
|
|
233
|
+
cache,
|
|
234
|
+
});
|
|
235
|
+
for (const file of pruned) {
|
|
236
|
+
console.log(` ${isDryRun ? "would prune" : "pruned"}: ${file}`);
|
|
237
|
+
}
|
|
238
|
+
if (pruned.length > 0) {
|
|
239
|
+
console.log(
|
|
240
|
+
`${pruned.length} orphaned translation(s)${isDryRun ? " would be" : ""} removed.`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
199
245
|
const pages = getEnglishMdxPages();
|
|
200
246
|
const filteredPages = args.pages
|
|
201
247
|
? pages.filter((p) => {
|
|
@@ -373,6 +419,12 @@ async function main() {
|
|
|
373
419
|
if (totalInput > 0) {
|
|
374
420
|
console.log(`Total tokens: ${totalInput} input + ${totalOutput} output`);
|
|
375
421
|
}
|
|
422
|
+
// Surface pages that needed a re-translation to pass validation, so a run
|
|
423
|
+
// that quietly retried does not read as a clean one in the job log.
|
|
424
|
+
const retried = translated.filter((r) => (r.attempts ?? 1) > 1);
|
|
425
|
+
if (retried.length > 0) {
|
|
426
|
+
console.log(`Retried: ${retried.length}`);
|
|
427
|
+
}
|
|
376
428
|
|
|
377
429
|
if (errors.length > 0) {
|
|
378
430
|
process.exit(1);
|
|
@@ -4,12 +4,21 @@ import {
|
|
|
4
4
|
mkdirSync,
|
|
5
5
|
readdirSync,
|
|
6
6
|
statSync,
|
|
7
|
+
existsSync,
|
|
8
|
+
rmSync,
|
|
7
9
|
} from "node:fs";
|
|
8
10
|
import { dirname, join, relative } from "node:path";
|
|
9
11
|
import { fileURLToPath } from "node:url";
|
|
10
12
|
import { getLanguageByCode } from "./config";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
+
import { translateValidated } from "./translator";
|
|
14
|
+
import { findTranslationError } from "./validate-translation";
|
|
15
|
+
import {
|
|
16
|
+
readCache,
|
|
17
|
+
writeCache,
|
|
18
|
+
isCached,
|
|
19
|
+
setCacheEntry,
|
|
20
|
+
getCacheKey,
|
|
21
|
+
} from "./cache";
|
|
13
22
|
import type { TranslationResult, TranslationCache } from "./types";
|
|
14
23
|
|
|
15
24
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -94,6 +103,60 @@ export function stripStrayTrailingFence(content: string): string {
|
|
|
94
103
|
return lines.join("\n");
|
|
95
104
|
}
|
|
96
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Convert HTML comments (`<!-- ... -->`) into MDX brace-slash-star comments.
|
|
108
|
+
*
|
|
109
|
+
* Mintlify parses every page as MDX, where a top-level HTML comment is a hard
|
|
110
|
+
* syntax error ("Unexpected character `!` (U+0021) before name …") that fails
|
|
111
|
+
* the whole deployment — the remedy Mintlify itself suggests is switching to
|
|
112
|
+
* MDX's own comment form. The root README keeps HTML-comment syntax (GitHub
|
|
113
|
+
* renders it invisibly), so its translated copies under docs/i18n/ have to be
|
|
114
|
+
* rewritten to the MDX form or the docs deploy breaks.
|
|
115
|
+
*
|
|
116
|
+
* Comments inside fenced code blocks are left untouched — there `<!-- -->` is
|
|
117
|
+
* literal sample text (e.g. the plist snippet in the AgentEye collector docs),
|
|
118
|
+
* not a comment to convert. Any `*`+`/` sequence inside a body is broken up so
|
|
119
|
+
* the generated JS block comment can't be terminated early.
|
|
120
|
+
*/
|
|
121
|
+
export function convertHtmlComments(content: string): string {
|
|
122
|
+
// Map out fenced-code ranges so comments inside them stay literal. Per
|
|
123
|
+
// CommonMark, a fence opens with ≥3 backticks or tildes and closes only on a
|
|
124
|
+
// later line using the SAME character and at least the same length. A naive
|
|
125
|
+
// "any ``` toggles" counter misfires on a ```` block that embeds ``` or on
|
|
126
|
+
// mixed ```/~~~ fences — the toggle desyncs and a real top-level comment
|
|
127
|
+
// after the block would be left unconverted (breaking the deploy we fix here).
|
|
128
|
+
const fenceRanges: Array<[number, number]> = [];
|
|
129
|
+
const fenceRe = /^[ \t]*(`{3,}|~{3,})/gm;
|
|
130
|
+
let fenceMatch: RegExpExecArray | null;
|
|
131
|
+
let open: { char: string; length: number; start: number } | null = null;
|
|
132
|
+
while ((fenceMatch = fenceRe.exec(content)) !== null) {
|
|
133
|
+
const marker = fenceMatch[1];
|
|
134
|
+
if (!open) {
|
|
135
|
+
open = { char: marker[0], length: marker.length, start: fenceMatch.index };
|
|
136
|
+
} else if (marker[0] === open.char && marker.length >= open.length) {
|
|
137
|
+
const lineEnd = content.indexOf("\n", fenceRe.lastIndex);
|
|
138
|
+
fenceRanges.push([open.start, lineEnd === -1 ? content.length : lineEnd]);
|
|
139
|
+
open = null;
|
|
140
|
+
}
|
|
141
|
+
// A different char or shorter marker while a fence is open is inner content.
|
|
142
|
+
}
|
|
143
|
+
// An unterminated fence runs to the end of the document.
|
|
144
|
+
if (open) fenceRanges.push([open.start, content.length]);
|
|
145
|
+
|
|
146
|
+
const isInsideFence = (offset: number): boolean =>
|
|
147
|
+
fenceRanges.some(([start, end]) => offset >= start && offset < end);
|
|
148
|
+
|
|
149
|
+
return content.replace(
|
|
150
|
+
/<!--([\s\S]*?)-->/g,
|
|
151
|
+
(match: string, body: string, offset: number) => {
|
|
152
|
+
if (isInsideFence(offset)) return match;
|
|
153
|
+
// Neutralize any `*/` so it can't close the JS block comment early.
|
|
154
|
+
const safeBody = body.replace(/\*\//g, "* /");
|
|
155
|
+
return `{/*${safeBody}*/}`;
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
97
160
|
/**
|
|
98
161
|
* Rewrite internal doc links to include the language prefix.
|
|
99
162
|
* e.g. href="/built-in-policies" -> href="/es/built-in-policies"
|
|
@@ -131,10 +194,13 @@ export async function translateMdxPage(
|
|
|
131
194
|
dryRun?: boolean;
|
|
132
195
|
model?: string;
|
|
133
196
|
cache?: TranslationCache;
|
|
197
|
+
/** Override the docs root. Tests point this at a fixture tree. */
|
|
198
|
+
docsDir?: string;
|
|
134
199
|
} = {},
|
|
135
200
|
): Promise<TranslationResult> {
|
|
136
|
-
const
|
|
137
|
-
const
|
|
201
|
+
const docsDir = options.docsDir ?? DOCS_DIR;
|
|
202
|
+
const relPath = relative(docsDir, sourcePath);
|
|
203
|
+
const outputPath = join(docsDir, lang, relPath);
|
|
138
204
|
const sourceContent = readFileSync(sourcePath, "utf-8");
|
|
139
205
|
|
|
140
206
|
const langConfig = getLanguageByCode(lang);
|
|
@@ -166,23 +232,31 @@ export async function translateMdxPage(
|
|
|
166
232
|
};
|
|
167
233
|
}
|
|
168
234
|
|
|
169
|
-
// Translate
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
235
|
+
// Translate and validate the exact bytes we will write. The render callback
|
|
236
|
+
// reproduces the historical sanitize + link-rewrite chain byte-for-byte
|
|
237
|
+
// (strip stray JSX-attribute quotes, drop an unmatched trailing fence,
|
|
238
|
+
// convert HTML comments to MDX, then add the language prefix to links), so
|
|
239
|
+
// the validated bytes ARE the written bytes and a pass here equals a pass in
|
|
240
|
+
// the deploy. On exhaustion translateValidated throws before we reach the
|
|
241
|
+
// write, so an invalid page is never written or cached.
|
|
242
|
+
const { rendered, inputTokens, outputTokens, attempts } =
|
|
243
|
+
await translateValidated({
|
|
244
|
+
source: sourceContent,
|
|
245
|
+
lang,
|
|
246
|
+
langName: langConfig.name,
|
|
247
|
+
model: options.model,
|
|
248
|
+
label: `${relPath} [${lang}]`,
|
|
249
|
+
render: (raw) =>
|
|
250
|
+
rewriteInternalLinks(
|
|
251
|
+
convertHtmlComments(stripStrayTrailingFence(sanitizeJsxAttributes(raw))),
|
|
252
|
+
lang,
|
|
253
|
+
),
|
|
254
|
+
validate: (bytes) => findTranslationError(bytes, sourceContent),
|
|
255
|
+
});
|
|
182
256
|
|
|
183
257
|
// Write output
|
|
184
258
|
mkdirSync(dirname(outputPath), { recursive: true });
|
|
185
|
-
writeFileSync(outputPath,
|
|
259
|
+
writeFileSync(outputPath, rendered);
|
|
186
260
|
|
|
187
261
|
// Update cache — skip if caller manages the cache (batch write)
|
|
188
262
|
if (!options.cache) {
|
|
@@ -205,6 +279,7 @@ export async function translateMdxPage(
|
|
|
205
279
|
inputTokens,
|
|
206
280
|
outputTokens,
|
|
207
281
|
cached: false,
|
|
282
|
+
attempts,
|
|
208
283
|
};
|
|
209
284
|
}
|
|
210
285
|
|
|
@@ -234,6 +309,71 @@ export function getEnglishMdxPages(): string[] {
|
|
|
234
309
|
return results.sort();
|
|
235
310
|
}
|
|
236
311
|
|
|
312
|
+
/** Every `.mdx` file under `dir`, recursively. */
|
|
313
|
+
function collectMdxFiles(dir: string): string[] {
|
|
314
|
+
const results: string[] = [];
|
|
315
|
+
for (const entry of readdirSync(dir)) {
|
|
316
|
+
const full = join(dir, entry);
|
|
317
|
+
if (statSync(full).isDirectory()) {
|
|
318
|
+
results.push(...collectMdxFiles(full));
|
|
319
|
+
} else if (entry.endsWith(".mdx")) {
|
|
320
|
+
results.push(full);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return results;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Delete translated pages whose English source no longer exists, and drop
|
|
328
|
+
* their cache entries.
|
|
329
|
+
*
|
|
330
|
+
* Translation only ever moves forward: `getEnglishMdxPages()` drives what gets
|
|
331
|
+
* written, so when an English page is deleted upstream (the agenteye sync does
|
|
332
|
+
* this routinely) its 14 translations are simply never revisited. They linger
|
|
333
|
+
* on disk and `--update-nav` drops them from `docs.json`, which hides them from
|
|
334
|
+
* the sidebar but does *not* unpublish them — Mintlify still serves and indexes
|
|
335
|
+
* any `.mdx` present, so non-English readers can land on a page documenting a
|
|
336
|
+
* removed feature with no navigation out. Left unpruned these also accumulate
|
|
337
|
+
* as permanent `validate:mdx` surface area for content no English source can
|
|
338
|
+
* ever correct.
|
|
339
|
+
*
|
|
340
|
+
* Returns the docs-dir-relative paths that were (or, when `dryRun`, would be)
|
|
341
|
+
* removed.
|
|
342
|
+
*/
|
|
343
|
+
export function pruneOrphanedTranslations(
|
|
344
|
+
langCodes: string[],
|
|
345
|
+
options: {
|
|
346
|
+
dryRun?: boolean;
|
|
347
|
+
cache?: TranslationCache;
|
|
348
|
+
/** Override the docs root. Tests point this at a fixture tree. */
|
|
349
|
+
docsDir?: string;
|
|
350
|
+
} = {},
|
|
351
|
+
): string[] {
|
|
352
|
+
const docsDir = options.docsDir ?? DOCS_DIR;
|
|
353
|
+
const removed: string[] = [];
|
|
354
|
+
|
|
355
|
+
for (const lang of langCodes) {
|
|
356
|
+
const langDir = join(docsDir, lang);
|
|
357
|
+
if (!existsSync(langDir)) continue;
|
|
358
|
+
|
|
359
|
+
for (const file of collectMdxFiles(langDir)) {
|
|
360
|
+
// docs/zh/agenteye/foo.mdx -> agenteye/foo.mdx -> docs/agenteye/foo.mdx
|
|
361
|
+
const relPath = relative(langDir, file);
|
|
362
|
+
if (existsSync(join(docsDir, relPath))) continue;
|
|
363
|
+
|
|
364
|
+
removed.push(relative(docsDir, file));
|
|
365
|
+
if (!options.dryRun) {
|
|
366
|
+
rmSync(file);
|
|
367
|
+
if (options.cache) {
|
|
368
|
+
delete options.cache.translations[getCacheKey(relPath, lang)];
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return removed.sort();
|
|
375
|
+
}
|
|
376
|
+
|
|
237
377
|
function isLanguageDir(name: string): boolean {
|
|
238
378
|
const langCodes = [
|
|
239
379
|
"zh",
|
|
@@ -2,8 +2,13 @@ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { LANGUAGES, getLanguageByCode } from "./config";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { translateValidated } from "./translator";
|
|
6
|
+
import {
|
|
7
|
+
stripStrayTrailingFence,
|
|
8
|
+
convertHtmlComments,
|
|
9
|
+
sanitizeJsxAttributes,
|
|
10
|
+
} from "./mdx-translator";
|
|
11
|
+
import { findTranslationError } from "./validate-translation";
|
|
7
12
|
import { readCache, writeCache, isCached, setCacheEntry } from "./cache";
|
|
8
13
|
import type { TranslationResult, TranslationCache } from "./types";
|
|
9
14
|
|
|
@@ -86,15 +91,12 @@ export async function translateReadme(
|
|
|
86
91
|
};
|
|
87
92
|
}
|
|
88
93
|
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
// Build the final output with header
|
|
94
|
+
// Compute the wrapper (disclaimer, language selector, RTL <div>) up front so
|
|
95
|
+
// the render callback can assemble the FINAL bytes on every attempt. The
|
|
96
|
+
// assembled bytes — not the raw model output — are what gets validated: the
|
|
97
|
+
// swallowed-`</div>` class is introduced by the wrapper AFTER the model
|
|
98
|
+
// returns, and `mintlify validate` never sees the README at all, so this gate
|
|
99
|
+
// is the only thing standing between a broken README and the deploy.
|
|
98
100
|
const disclaimer = langConfig.rtl
|
|
99
101
|
? `> **\u26a0\ufe0f** \u0647\u0630\u0647 \u062a\u0631\u062c\u0645\u0629 \u0622\u0644\u064a\u0629. \u0644\u0644\u0627\u0637\u0644\u0627\u0639 \u0639\u0644\u0649 \u0623\u062d\u062f\u062b \u0625\u0635\u062f\u0627\u0631\u060c \u0631\u0627\u062c\u0639 [English README](../../README.md).`
|
|
100
102
|
: `> **\u26a0\ufe0f** This is an auto-generated translation. For the latest version, see the [English README](../../README.md). Community corrections welcome!`;
|
|
@@ -103,16 +105,31 @@ export async function translateReadme(
|
|
|
103
105
|
const rtlOpen = langConfig.rtl ? `<div dir="rtl">\n\n` : "";
|
|
104
106
|
const rtlClose = langConfig.rtl ? `\n\n</div>` : "";
|
|
105
107
|
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
108
|
+
// Translate and validate the assembled bytes, re-translating on failure.
|
|
109
|
+
const { rendered, inputTokens, outputTokens, attempts } =
|
|
110
|
+
await translateValidated({
|
|
111
|
+
source: sourceContent,
|
|
112
|
+
lang,
|
|
113
|
+
langName: langConfig.name,
|
|
114
|
+
model: options.model,
|
|
115
|
+
label: `README.${lang}.md`,
|
|
116
|
+
render: (raw) => {
|
|
117
|
+
// Same MDX sanitizers as translateMdxPage — the README emits JSX (the
|
|
118
|
+
// logo table), so strip stray attribute quotes, drop any unmatched
|
|
119
|
+
// trailing code fence (which would swallow the RTL `</div>`), and
|
|
120
|
+
// convert HTML comments to MDX — then wrap in disclaimer + selector +
|
|
121
|
+
// RTL div.
|
|
122
|
+
const cleaned = convertHtmlComments(
|
|
123
|
+
stripStrayTrailingFence(sanitizeJsxAttributes(raw)),
|
|
124
|
+
);
|
|
125
|
+
return `${disclaimer}\n\n${langSelector}\n\n---\n${rtlOpen}\n${cleaned}\n${rtlClose}`;
|
|
126
|
+
},
|
|
127
|
+
validate: (bytes) => findTranslationError(bytes, sourceContent),
|
|
128
|
+
});
|
|
112
129
|
|
|
113
130
|
// Write output
|
|
114
131
|
mkdirSync(I18N_DIR, { recursive: true });
|
|
115
|
-
writeFileSync(outputPath,
|
|
132
|
+
writeFileSync(outputPath, rendered);
|
|
116
133
|
|
|
117
134
|
// Update cache — skip if caller manages the cache (batch write)
|
|
118
135
|
if (!options.cache) {
|
|
@@ -128,6 +145,7 @@ export async function translateReadme(
|
|
|
128
145
|
inputTokens,
|
|
129
146
|
outputTokens,
|
|
130
147
|
cached: false,
|
|
148
|
+
attempts,
|
|
131
149
|
};
|
|
132
150
|
}
|
|
133
151
|
|
|
@@ -3,6 +3,47 @@ import { DO_NOT_TRANSLATE } from "./config";
|
|
|
3
3
|
|
|
4
4
|
let client: Anthropic | null = null;
|
|
5
5
|
|
|
6
|
+
// Output-token ceiling for a single translation. The largest English docs
|
|
7
|
+
// (e.g. agenteye/kubernetes-deployment.mdx at ~1400 lines) translate to well
|
|
8
|
+
// beyond the old 16384 cap for verbose target languages. When a response hit
|
|
9
|
+
// that cap it was silently truncated mid-MDX — leaving an unbalanced `{` or an
|
|
10
|
+
// unterminated JSX expression — and the partial output was written to disk and
|
|
11
|
+
// the cache, only to fail `mintlify validate` later in the consolidate job
|
|
12
|
+
// (or, for the very largest pages, to trip the proxy with a "stream ended"
|
|
13
|
+
// error). 64000 is Claude Haiku 4.5's max output (Tier 2/3 languages) and well
|
|
14
|
+
// inside Claude Sonnet 4.6's 128000 (Tier 1), with headroom over the largest
|
|
15
|
+
// observed translation; max_tokens is only a ceiling, so smaller pages still
|
|
16
|
+
// stop at `end_turn` and cost the same. Streaming (below) is what keeps an
|
|
17
|
+
// output this large safe from the SDK's HTTP timeout.
|
|
18
|
+
// Override via TRANSLATE_MAX_TOKENS (integer >= 1; otherwise default).
|
|
19
|
+
const parsedMaxTokens = Number.parseInt(
|
|
20
|
+
process.env.TRANSLATE_MAX_TOKENS ?? "",
|
|
21
|
+
10,
|
|
22
|
+
);
|
|
23
|
+
const MAX_TOKENS =
|
|
24
|
+
Number.isInteger(parsedMaxTokens) && parsedMaxTokens > 0
|
|
25
|
+
? parsedMaxTokens
|
|
26
|
+
: 64000;
|
|
27
|
+
|
|
28
|
+
// Maximum TOTAL validation attempts per page: 1 initial translation plus up to
|
|
29
|
+
// MAX_ATTEMPTS-1 re-translations when the rendered output fails validation
|
|
30
|
+
// (see translateValidated below). Distinct from the SDK transport budget
|
|
31
|
+
// TRANSLATE_MAX_RETRIES (default 5, in getClient): that retries a single HTTP
|
|
32
|
+
// request on a connection error; this re-translates a page whose *content*
|
|
33
|
+
// failed the docs-build checks. They multiply — at most
|
|
34
|
+
// MAX_ATTEMPTS x (1 + maxRetries) HTTP requests for one page in the worst case.
|
|
35
|
+
// 3 collapses the observed per-page failure rate to negligible while costing
|
|
36
|
+
// wall-clock (a serial retry inside one worker slot), not peak concurrency.
|
|
37
|
+
// Override via TRANSLATE_MAX_ATTEMPTS (integer >= 1; 1 disables retries).
|
|
38
|
+
const parsedMaxAttempts = Number.parseInt(
|
|
39
|
+
process.env.TRANSLATE_MAX_ATTEMPTS ?? "",
|
|
40
|
+
10,
|
|
41
|
+
);
|
|
42
|
+
const MAX_ATTEMPTS =
|
|
43
|
+
Number.isInteger(parsedMaxAttempts) && parsedMaxAttempts > 0
|
|
44
|
+
? parsedMaxAttempts
|
|
45
|
+
: 3;
|
|
46
|
+
|
|
6
47
|
function getClient(): Anthropic {
|
|
7
48
|
if (!client) {
|
|
8
49
|
// Default 5 retries (up from SDK default of 2) so transient
|
|
@@ -22,7 +63,7 @@ const SYSTEM_PROMPT = `You are a professional technical documentation translator
|
|
|
22
63
|
|
|
23
64
|
1. **Preserve all code blocks exactly as-is** — never translate content inside backtick-fenced code blocks (\`\`\`...\`\`\`) or inline code (\`...\`).
|
|
24
65
|
2. **Preserve MDX component syntax** — tags like <Card>, <CardGroup>, <CodeGroup>, <Steps>, <Step>, <Note>, <Tip>, <Tabs>, <Tab>, <Warning> must remain unchanged. Their attribute names (title, icon, href, cols) must remain in English. Only translate the text content of the \`title\` attribute and the text body between tags. **Never put an ASCII straight \`"\` inside a \`title="…"\` (or any JSX attribute value)** — it terminates the attribute and breaks MDX parsing. If the target language would normally wrap a word in quotation marks (e.g. German „…", Japanese 「…」), drop the inner quotes inside attribute values and rely on the surrounding tag for emphasis.
|
|
25
|
-
3. **Preserve YAML frontmatter keys** — only translate the string values of \`title\` and \`description\`. Keep the \`icon\` value unchanged.
|
|
66
|
+
3. **Preserve YAML frontmatter keys** — only translate the string values of \`title\` and \`description\`. Keep the \`icon\` value unchanged. Never rename, add, or drop a frontmatter key. The \`title\` and \`description\` values are wrapped in double quotes: **never put an unescaped ASCII \`"\` inside them** — it terminates the YAML string and breaks the frontmatter parse (exactly as an ASCII \`"\` breaks a JSX attribute in rule 2). If the source value contains an escaped quote (\`\\"\`), keep it escaped in the same form; if the target language would quote a phrase, use typographic quotes (e.g. „…", «…», 「…」) or rephrase to avoid the inner quote.
|
|
26
67
|
4. **Preserve all URLs and paths** — never modify href values, image paths, or links.
|
|
27
68
|
5. **Preserve Markdown structure** — headers (#, ##), lists (-, *), tables (|), bold (**), italic (*), links ([text](url)) must keep their Markdown formatting.
|
|
28
69
|
6. **Preserve badge/shield URLs** — any [](url) pattern must remain completely unchanged.
|
|
@@ -43,11 +84,40 @@ ${DO_NOT_TRANSLATE.map((t) => `- ${t}`).join("\n")}
|
|
|
43
84
|
|
|
44
85
|
Return ONLY the translated content. Do not add explanations, notes, or commentary.`;
|
|
45
86
|
|
|
87
|
+
export interface RetryFeedback {
|
|
88
|
+
attempt: number;
|
|
89
|
+
maxAttempts: number;
|
|
90
|
+
/** The previous attempt's validation error — model-actionable text. */
|
|
91
|
+
error: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The repair note appended to the user turn on a retry. The failed attempt is
|
|
96
|
+
* NOT fed back as an assistant turn: a fresh re-translate keeps the input flat
|
|
97
|
+
* across attempts. Conversational repair would grow the input every attempt and
|
|
98
|
+
* push a large README toward the max_tokens ceiling on exactly the retry where
|
|
99
|
+
* a truncation would be worst — and the error is self-locating (it carries the
|
|
100
|
+
* caret excerpt / body snippet), so the model needs the note, not its own prior
|
|
101
|
+
* output, to fix the defect.
|
|
102
|
+
*/
|
|
103
|
+
function buildRetryFeedback(f: RetryFeedback): string {
|
|
104
|
+
return (
|
|
105
|
+
`[Retry ${f.attempt} of ${f.maxAttempts}. The source document above is unchanged.]\n\n` +
|
|
106
|
+
"Your previous translation of this document was REJECTED by the docs build " +
|
|
107
|
+
"and discarded. It failed validation with:\n\n" +
|
|
108
|
+
`${f.error}\n\n` +
|
|
109
|
+
"Translate the source again from the beginning and return ONLY the " +
|
|
110
|
+
"corrected translation — do not comment on this note. Do not reproduce that " +
|
|
111
|
+
"defect. Every rule above still applies."
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
46
115
|
export async function translateContent(
|
|
47
116
|
content: string,
|
|
48
117
|
targetLang: string,
|
|
49
118
|
targetLangName: string,
|
|
50
119
|
model: string = "claude-sonnet-4-6",
|
|
120
|
+
feedback?: RetryFeedback,
|
|
51
121
|
): Promise<{ translated: string; inputTokens: number; outputTokens: number }> {
|
|
52
122
|
const anthropic = getClient();
|
|
53
123
|
|
|
@@ -59,20 +129,36 @@ export async function translateContent(
|
|
|
59
129
|
// surfaces to the SDK as `APIConnectionError ("Connection error.")`.
|
|
60
130
|
// `messages.stream(...).finalMessage()` returns the same Message shape
|
|
61
131
|
// as `messages.create(...)`, so the rest of the pipeline is unchanged.
|
|
132
|
+
const base = `Translate the following documentation content into ${targetLangName} (${targetLang}).\n\n---\n\n${content}`;
|
|
133
|
+
// On a retry, append the repair note after the source in the SAME single user
|
|
134
|
+
// turn — no assistant turn is fed back, so the request stays flat-input.
|
|
135
|
+
const userContent = feedback
|
|
136
|
+
? `${base}\n\n---\n\n${buildRetryFeedback(feedback)}`
|
|
137
|
+
: base;
|
|
138
|
+
|
|
62
139
|
const response = await anthropic.messages.stream({
|
|
63
140
|
model,
|
|
64
|
-
max_tokens:
|
|
141
|
+
max_tokens: MAX_TOKENS,
|
|
65
142
|
system: [{ type: "text", text: SYSTEM_PROMPT, cache_control: { type: "ephemeral" } }],
|
|
66
|
-
messages: [
|
|
67
|
-
{
|
|
68
|
-
role: "user",
|
|
69
|
-
content: `Translate the following documentation content into ${targetLangName} (${targetLang}).\n\n---\n\n${content}`,
|
|
70
|
-
},
|
|
71
|
-
],
|
|
143
|
+
messages: [{ role: "user", content: userContent }],
|
|
72
144
|
}).finalMessage();
|
|
73
145
|
|
|
146
|
+
// A truncated translation is worse than a failed one: the model stops
|
|
147
|
+
// mid-MDX, and that partial would otherwise be written to disk and cached as
|
|
148
|
+
// if complete, surfacing only as an unbalanced-brace parse error in
|
|
149
|
+
// `mintlify validate`. Fail loudly instead so the caller (cli.ts) records an
|
|
150
|
+
// error, never caches the partial, and excludes this language from the
|
|
151
|
+
// consolidate publish step. If a page ever legitimately needs more than
|
|
152
|
+
// MAX_TOKENS, raise TRANSLATE_MAX_TOKENS or split the source.
|
|
153
|
+
if (response.stop_reason === "max_tokens") {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`translation truncated at max_tokens=${MAX_TOKENS} ` +
|
|
156
|
+
`(output ${response.usage.output_tokens} tokens) — source too large to translate in one request`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
74
160
|
const translated =
|
|
75
|
-
response.content[0]
|
|
161
|
+
response.content[0]?.type === "text" ? response.content[0].text : "";
|
|
76
162
|
|
|
77
163
|
return {
|
|
78
164
|
translated,
|
|
@@ -80,3 +166,88 @@ export async function translateContent(
|
|
|
80
166
|
outputTokens: response.usage.output_tokens,
|
|
81
167
|
};
|
|
82
168
|
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Translate `source`, render it to the exact bytes that will be written, and
|
|
172
|
+
* validate those bytes — re-translating with the validation error fed back
|
|
173
|
+
* until it passes or MAX_ATTEMPTS is reached.
|
|
174
|
+
*
|
|
175
|
+
* The loop lives here, not in translateContent, because each caller renders
|
|
176
|
+
* different final bytes (the MDX pages add rewriteInternalLinks; the README
|
|
177
|
+
* wraps the body in a disclaimer + RTL `<div>`). Validating the RENDERED bytes —
|
|
178
|
+
* not the raw model output — is what makes a pass here equal to a pass in
|
|
179
|
+
* `mintlify validate` and the deploy.
|
|
180
|
+
*
|
|
181
|
+
* On exhaustion it THROWS (never returns a partial), so the caller's write is
|
|
182
|
+
* unreachable and no invalid page is ever written or cached — the same
|
|
183
|
+
* fail-loud contract translateContent already enforces for max_tokens
|
|
184
|
+
* truncation. Transport/auth/max_tokens errors from translateContent propagate
|
|
185
|
+
* unchanged and consume no attempt: only *validity* failures retry.
|
|
186
|
+
*/
|
|
187
|
+
export async function translateValidated(opts: {
|
|
188
|
+
source: string;
|
|
189
|
+
lang: string;
|
|
190
|
+
langName: string;
|
|
191
|
+
model?: string;
|
|
192
|
+
/** Label for the per-attempt warning line, e.g. `agenteye/cli.mdx [de]`. */
|
|
193
|
+
label: string;
|
|
194
|
+
/** Turn raw model output into the exact bytes that will be written. */
|
|
195
|
+
render: (raw: string) => string;
|
|
196
|
+
/** Validate the rendered bytes; return an error message, or null if valid. */
|
|
197
|
+
validate: (rendered: string) => Promise<string | null>;
|
|
198
|
+
}): Promise<{
|
|
199
|
+
rendered: string;
|
|
200
|
+
inputTokens: number;
|
|
201
|
+
outputTokens: number;
|
|
202
|
+
attempts: number;
|
|
203
|
+
}> {
|
|
204
|
+
let inputTokens = 0;
|
|
205
|
+
let outputTokens = 0;
|
|
206
|
+
let lastError = "";
|
|
207
|
+
|
|
208
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
209
|
+
const feedback: RetryFeedback | undefined =
|
|
210
|
+
attempt > 1
|
|
211
|
+
? { attempt, maxAttempts: MAX_ATTEMPTS, error: lastError }
|
|
212
|
+
: undefined;
|
|
213
|
+
|
|
214
|
+
// No try/catch: a transport/auth/max_tokens throw is not a validity
|
|
215
|
+
// failure — let it propagate so it is never silently retried as one.
|
|
216
|
+
const result = await translateContent(
|
|
217
|
+
opts.source,
|
|
218
|
+
opts.lang,
|
|
219
|
+
opts.langName,
|
|
220
|
+
opts.model,
|
|
221
|
+
feedback,
|
|
222
|
+
);
|
|
223
|
+
inputTokens += result.inputTokens;
|
|
224
|
+
outputTokens += result.outputTokens;
|
|
225
|
+
|
|
226
|
+
if (result.translated.trim() === "") {
|
|
227
|
+
// An empty response is a validity failure, not a usable page — resample
|
|
228
|
+
// rather than write a blank file.
|
|
229
|
+
lastError =
|
|
230
|
+
"The translation was empty. Return the full translated document.";
|
|
231
|
+
console.warn(
|
|
232
|
+
` ${opts.label} -> attempt ${attempt}/${MAX_ATTEMPTS} produced empty output; retrying`,
|
|
233
|
+
);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const rendered = opts.render(result.translated);
|
|
238
|
+
const error = await opts.validate(rendered);
|
|
239
|
+
if (error === null) {
|
|
240
|
+
return { rendered, inputTokens, outputTokens, attempts: attempt };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
lastError = error;
|
|
244
|
+
console.warn(
|
|
245
|
+
` ${opts.label} -> attempt ${attempt}/${MAX_ATTEMPTS} failed validation: ${error.split("\n")[0]}`,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
throw new Error(
|
|
250
|
+
`translation into ${opts.langName} (${opts.lang}) still fails validation ` +
|
|
251
|
+
`after ${MAX_ATTEMPTS} attempt(s): ${lastError.split("\n")[0]}`,
|
|
252
|
+
);
|
|
253
|
+
}
|