Skip to content

Commit a78d51f

Browse files
committed
Add update.mts to update docs and translations
1 parent 4148a33 commit a78d51f

File tree

1 file changed

+228
-0
lines changed

1 file changed

+228
-0
lines changed

update.mts

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
#!/usr/bin/env -S deno run --allow-net --allow-read --allow-write --allow-run=bash,unzip --allow-env
2+
3+
// This script fetches the latest docs from Notion and translations from Crowdin,
4+
// then copies the files to the appropriate directories.
5+
6+
// One current limitation is that when the docs from Notion are ahead of the translations, it can result in broken links between the two.
7+
// A possible remediation would be to copy the English files to the other locales first, then overwrite those with the translated files.
8+
// This doesn't fully solve the problem though, because metadata in the translated files may be different from the English files.
9+
10+
import { copy, readerFromStreamReader } from "jsr:@std/io";
11+
12+
const projectId = Deno.env.get("CROWDIN_PROJECT_ID");
13+
const apiKey = Deno.env.get("CROWDIN_API_KEY");
14+
const projectRoot = Deno.cwd();
15+
16+
function ensureSuccess(response: Response) {
17+
if (!response.ok)
18+
throw new Error(
19+
`Request for ${response.url} failed with status code ${response.status}`
20+
);
21+
}
22+
23+
async function saveLatestBuild() {
24+
// Create a new build
25+
const response = await fetch(
26+
`https://api.crowdin.com/api/v2/projects/${projectId}/translations/builds`,
27+
{
28+
method: "POST",
29+
headers: {
30+
Authorization: `Bearer ${apiKey}`,
31+
"Content-type": "application/json",
32+
},
33+
body: JSON.stringify({
34+
skipUntranslatedStrings: false,
35+
skipUntranslatedFiles: false,
36+
exportApprovedOnly: false,
37+
}),
38+
}
39+
);
40+
41+
ensureSuccess(response);
42+
const buildResponseBody = await response.json();
43+
44+
const buildId = buildResponseBody.data.id;
45+
46+
// Poll the build status
47+
let finished = false;
48+
while (finished === false) {
49+
console.log("Checking build status...");
50+
51+
const buildStatusResponse = await fetch(
52+
`https://api.crowdin.com/api/v2/projects/${projectId}/translations/builds/${buildId}`,
53+
{ headers: { Authorization: `Bearer ${apiKey}` } }
54+
);
55+
56+
ensureSuccess(buildStatusResponse);
57+
const buildStatusBody = await buildStatusResponse.json();
58+
59+
if (buildStatusBody.data.status === "finished") {
60+
finished = true;
61+
} else if (buildStatusBody.data.status === "inProgress") {
62+
console.log(
63+
`Build status: ${buildStatusBody.data.status}. Waiting for 5 seconds...`
64+
);
65+
await new Promise((resolve) => setTimeout(resolve, 5000));
66+
} else {
67+
throw new Error(
68+
`Unexpected build status: ${buildStatusBody.data.status}`
69+
);
70+
}
71+
}
72+
73+
console.log("Build finished!");
74+
75+
const buildDownloadResponse = await fetch(
76+
`https://api.crowdin.com/api/v2/projects/${projectId}/translations/builds/${buildId}/download`,
77+
{ headers: { Authorization: `Bearer ${apiKey}` } }
78+
);
79+
80+
ensureSuccess(buildDownloadResponse);
81+
const buildDownloadBody = await buildDownloadResponse.json();
82+
const buildUrl = buildDownloadBody.data.url;
83+
84+
// Download and save the file
85+
console.log("Downloading the build:");
86+
console.log(buildUrl);
87+
const downloadResponse = await fetch(buildUrl);
88+
ensureSuccess(downloadResponse);
89+
90+
const buffer = await downloadResponse.bytes();
91+
92+
const zipFilePath = `${projectRoot}/translations.zip`;
93+
await Deno.writeFile(zipFilePath, buffer);
94+
95+
console.log("File downloaded and saved to", zipFilePath);
96+
97+
// Extract the zip file
98+
const command = new Deno.Command("unzip", {
99+
args: ["-o", zipFilePath, "-d", `${projectRoot}/translations`],
100+
stdout: "piped",
101+
stderr: "piped",
102+
});
103+
104+
const { code, stderr } = await command.output();
105+
if (code === 0) {
106+
console.log("Extraction completed successfully.");
107+
} else {
108+
const errorString = new TextDecoder().decode(stderr);
109+
console.error(errorString);
110+
throw new Error("ZIP extraction failed");
111+
}
112+
}
113+
114+
const helpLocales = [
115+
{
116+
docusaurus: "de",
117+
crowdin: "de",
118+
},
119+
{
120+
docusaurus: "es",
121+
crowdin: "es-ES",
122+
},
123+
{
124+
docusaurus: "fr",
125+
crowdin: "fr",
126+
},
127+
{
128+
docusaurus: "pt-BR",
129+
crowdin: "pt-BR",
130+
},
131+
] as const;
132+
133+
async function copyFiles() {
134+
for (const locale of helpLocales) {
135+
const source = `${projectRoot}/translations/${locale.crowdin}/Guides`;
136+
const dest = `${projectRoot}/i18n/${locale.docusaurus}/docusaurus-plugin-content-docs/current/`;
137+
138+
console.log(`Copying files from ${source} to ${dest}`);
139+
140+
await Deno.mkdir(dest, { recursive: true });
141+
142+
for await (const dirEntry of Deno.readDir(source)) {
143+
if (dirEntry.isFile) {
144+
const oldFile = `${source}/${dirEntry.name}`;
145+
const newFile = `${dest}/${dirEntry.name}`;
146+
await Deno.copyFile(oldFile, newFile);
147+
}
148+
}
149+
150+
for await (const dirEntry of Deno.readDir(`${projectRoot}/docs`)) {
151+
if (dirEntry.isFile && dirEntry.name.endsWith(".png")) {
152+
const oldFile = `${projectRoot}/docs/${dirEntry.name}`;
153+
const newFile = `${dest}/${dirEntry.name}`;
154+
await Deno.copyFile(oldFile, newFile);
155+
}
156+
}
157+
}
158+
}
159+
160+
async function cleanup() {
161+
await Deno.remove(`${projectRoot}/translations.zip`);
162+
await Deno.remove(`${projectRoot}/translations`, { recursive: true });
163+
}
164+
165+
async function deleteExistingFiles() {
166+
const docsDir = `${projectRoot}/docs`;
167+
console.log(`Deleting most files in ${docsDir}`);
168+
// delete all files except docs/getting-started.json and docs/getting-started.mdx
169+
170+
const filesToKeep = ["getting-started.json", "getting-started.mdx"];
171+
172+
for await (const dirEntry of Deno.readDir(docsDir)) {
173+
if (dirEntry.isFile && !filesToKeep.includes(dirEntry.name)) {
174+
await Deno.remove(`${docsDir}/${dirEntry.name}`);
175+
}
176+
}
177+
178+
const i18nDir = `${projectRoot}/i18n`;
179+
console.log(`Deleting ${i18nDir}`);
180+
await Deno.remove(i18nDir, { recursive: true });
181+
}
182+
183+
async function fetchNotionDocs() {
184+
const child = new Deno.Command("bash", {
185+
args: ["pull_docs.sh"],
186+
stdout: "piped",
187+
stderr: "piped",
188+
}).spawn();
189+
190+
copy(readerFromStreamReader(child.stdout.getReader()), Deno.stdout);
191+
copy(readerFromStreamReader(child.stderr.getReader()), Deno.stderr);
192+
193+
const status = await child.status;
194+
195+
if (!status.success) {
196+
const code = status.code;
197+
throw new Error(
198+
`Failed to fetch Notion docs. pull_docs.sh exited with code ${code}`
199+
);
200+
}
201+
}
202+
203+
try {
204+
console.log("--- Deleting existing files ---");
205+
await deleteExistingFiles();
206+
console.log();
207+
208+
console.log("--- Fetching latest docs from Notion ---");
209+
await fetchNotionDocs();
210+
console.log();
211+
212+
console.log("--- Fetching latest translations from Crowdin ---");
213+
await saveLatestBuild();
214+
console.log();
215+
216+
console.log("--- Copying files to i18n directory ---");
217+
await copyFiles();
218+
console.log();
219+
220+
console.log("--- Completed successfully ---");
221+
} catch (e) {
222+
console.error(e);
223+
} finally {
224+
console.log("--- Cleaning up ---");
225+
await cleanup();
226+
console.log();
227+
console.log("--- Done ---");
228+
}

0 commit comments

Comments
 (0)