-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsync-crowdin-distribution.js
More file actions
173 lines (150 loc) · 5.06 KB
/
Copy pathsync-crowdin-distribution.js
File metadata and controls
173 lines (150 loc) · 5.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
#!/usr/bin/env node
/**
* Syncs Crowdin distribution files from distributions.crowdin.net to a local directory.
* Designed to be run from GitHub Actions and produce a static-file artifact for GitHub Pages.
*
* Usage:
* node sync-crowdin-distribution.js
*
* Environment variables:
* OUTPUT_DIR - Directory to write files into (default: dist-pages/crowdin-dist)
*/
'use strict';
const https = require('node:https');
const fs = require('node:fs');
const path = require('node:path');
const BASE_CDN = 'https://distributions.crowdin.net';
const OUTPUT_DIR = path.resolve(process.env.OUTPUT_DIR || 'dist-pages/crowdin-dist');
/** Number of simultaneous downloads per batch. */
const CONCURRENCY = 8;
/**
* Distribution hashes to sync.
* Read from the CROWDIN_DISTRIBUTION_IDS environment variable as a
* comma-separated list (e.g. "hash1,hash2"). Store the value in GitHub
* project variables under the name CROWDIN_DISTRIBUTION_IDS.
*/
const DISTRIBUTIONS = (process.env.CROWDIN_DISTRIBUTION_IDS || '')
.split(',')
.map((s) => s.trim())
.filter(Boolean);
if (DISTRIBUTIONS.length === 0) {
console.error('ERROR: CROWDIN_DISTRIBUTION_IDS environment variable is not set or empty.');
process.exit(1);
}
/**
* Fetches a URL, following redirects, and returns the body as a Buffer.
* @param {string} url
* @returns {Promise<Buffer>}
*/
function fetchUrl(url) {
return new Promise((resolve, reject) => {
https.get(url, (res) => {
// Follow redirects
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
return fetchUrl(res.headers.location).then(resolve).catch(reject);
}
const chunks = [];
res.on('data', (chunk) => chunks.push(chunk));
res.on('end', () => {
if (res.statusCode >= 400) {
return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
}
resolve(Buffer.concat(chunks));
});
res.on('error', reject);
}).on('error', reject);
});
}
/**
* Writes data to a file, creating parent directories as needed.
* @param {string} filePath
* @param {Buffer|string} data
*/
function saveFile(filePath, data) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, data);
}
/**
* Processes an array of items in fixed-size concurrent batches.
* @template T
* @param {T[]} items
* @param {number} batchSize
* @param {(item: T) => Promise<void>} fn
*/
async function processInBatches(items, batchSize, fn) {
for (let i = 0; i < items.length; i += batchSize) {
await Promise.all(items.slice(i, i + batchSize).map(fn));
}
}
/**
* Downloads all distribution files for a single hash.
* @param {string} hash Distribution hash.
* @returns {Promise<boolean>} true if all files were fetched without errors.
*/
async function syncDistribution(hash) {
console.log(`\n=== Syncing distribution: ${hash} ===`);
const hashDir = path.join(OUTPUT_DIR, hash);
// manifest.json
console.log(' Fetching manifest.json...');
const manifestBuf = await fetchUrl(`${BASE_CDN}/${hash}/manifest.json`);
saveFile(path.join(hashDir, 'manifest.json'), manifestBuf);
const manifest = JSON.parse(manifestBuf.toString('utf8'));
console.log(` Timestamp : ${manifest.timestamp}`);
console.log(` Languages : ${(manifest.languages || []).length}`);
// languages.json
console.log(' Fetching languages.json...');
const langsBuf = await fetchUrl(`${BASE_CDN}/${hash}/languages.json`);
saveFile(path.join(hashDir, 'languages.json'), langsBuf);
// content files
const contentPaths = new Set();
if (manifest.content) {
for (const paths of Object.values(manifest.content)) {
for (const p of paths) {
contentPaths.add(p);
}
}
}
const pathList = [...contentPaths];
console.log(` Content files: ${pathList.length} (concurrency=${CONCURRENCY})`);
let fetched = 0;
let failed = 0;
await processInBatches(pathList, CONCURRENCY, async (contentPath) => {
const url = `${BASE_CDN}/${hash}${contentPath}`;
const localPath = path.join(hashDir, contentPath);
try {
const data = await fetchUrl(url);
saveFile(localPath, data);
fetched++;
if ((fetched + failed) % 50 === 0) {
console.log(` Progress: ${fetched + failed}/${pathList.length}`);
}
} catch (err) {
failed++;
console.warn(` WARN: failed to fetch ${contentPath}: ${err.message}`);
}
});
console.log(` Result: ${fetched} fetched, ${failed} failed`);
return failed === 0;
}
async function main() {
console.log('Crowdin Distribution Sync');
console.log(`Output dir: ${OUTPUT_DIR}`);
console.log(`Distributions: ${DISTRIBUTIONS.length}`);
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
let allOk = true;
for (const hash of DISTRIBUTIONS) {
try {
const ok = await syncDistribution(hash);
if (!ok) allOk = false;
} catch (err) {
console.error(`\nFATAL: Failed to sync ${hash}:`, err.message);
allOk = false;
}
}
if (!allOk) {
console.error('\nSync completed with errors.');
process.exit(1);
}
console.log('\nSync complete!');
}
main();