pxt-core 9.2.9 → 9.2.10

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.
@@ -0,0 +1,374 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.restoreFileBefore = exports.downloadFileTranslationsAsync = exports.downloadTranslationsAsync = exports.listFilesAsync = exports.getFileProgressAsync = exports.getDirectoryProgressAsync = exports.getProjectProgressAsync = exports.getProjectInfoAsync = exports.uploadFileAsync = exports.setProjectId = void 0;
4
+ const crowdin_api_client_1 = require("@crowdin/crowdin-api-client");
5
+ const path = require("path");
6
+ const axios_1 = require("axios");
7
+ const AdmZip = require("adm-zip");
8
+ let client;
9
+ const KINDSCRIPT_PROJECT_ID = 157956;
10
+ let projectId = KINDSCRIPT_PROJECT_ID;
11
+ let fetchedFiles;
12
+ let fetchedDirectories;
13
+ function setProjectId(id) {
14
+ projectId = id;
15
+ fetchedFiles = undefined;
16
+ fetchedDirectories = undefined;
17
+ }
18
+ exports.setProjectId = setProjectId;
19
+ async function uploadFileAsync(fileName, fileContent) {
20
+ if (pxt.crowdin.testMode)
21
+ return;
22
+ const files = await getAllFiles();
23
+ // If file already exists, update it
24
+ for (const file of files) {
25
+ if (normalizePath(file.path) === normalizePath(fileName)) {
26
+ await updateFile(file.id, path.basename(fileName), fileContent);
27
+ return;
28
+ }
29
+ }
30
+ // Ensure directory exists
31
+ const parentDir = path.dirname(fileName);
32
+ let parentDirId;
33
+ if (parentDir && parentDir !== ".") {
34
+ parentDirId = (await mkdirAsync(parentDir)).id;
35
+ }
36
+ // Create new file
37
+ await createFile(path.basename(fileName), fileContent, parentDirId);
38
+ }
39
+ exports.uploadFileAsync = uploadFileAsync;
40
+ async function getProjectInfoAsync() {
41
+ const { projectsGroupsApi } = getClient();
42
+ const project = await projectsGroupsApi.getProject(projectId);
43
+ return project.data;
44
+ }
45
+ exports.getProjectInfoAsync = getProjectInfoAsync;
46
+ async function getProjectProgressAsync(languages) {
47
+ const { translationStatusApi } = getClient();
48
+ const stats = await translationStatusApi
49
+ .withFetchAll()
50
+ .getProjectProgress(projectId);
51
+ let results = stats.data.map(stat => stat.data);
52
+ if (languages) {
53
+ results = results.filter(stat => languages.indexOf(stat.language.locale) !== -1 || languages.indexOf(stat.language.twoLettersCode) !== -1);
54
+ }
55
+ return results;
56
+ }
57
+ exports.getProjectProgressAsync = getProjectProgressAsync;
58
+ async function getDirectoryProgressAsync(directory, languages) {
59
+ const { translationStatusApi } = getClient();
60
+ const directoryId = await getDirectoryIdAsync(directory);
61
+ const stats = await translationStatusApi
62
+ .withFetchAll()
63
+ .getDirectoryProgress(projectId, directoryId);
64
+ let results = stats.data.map(stat => stat.data);
65
+ if (languages) {
66
+ results = results.filter(stat => languages.indexOf(stat.language.locale) !== -1 || languages.indexOf(stat.language.twoLettersCode) !== -1);
67
+ }
68
+ return results;
69
+ }
70
+ exports.getDirectoryProgressAsync = getDirectoryProgressAsync;
71
+ async function getFileProgressAsync(file, languages) {
72
+ const { translationStatusApi } = getClient();
73
+ const fileId = await getFileIdAsync(file);
74
+ const stats = await translationStatusApi
75
+ .withFetchAll()
76
+ .getFileProgress(projectId, fileId);
77
+ let results = stats.data.map(stat => stat.data);
78
+ if (languages) {
79
+ results = results.filter(stat => languages.indexOf(stat.language.locale) !== -1 || languages.indexOf(stat.language.twoLettersCode) !== -1);
80
+ }
81
+ return results;
82
+ }
83
+ exports.getFileProgressAsync = getFileProgressAsync;
84
+ async function listFilesAsync(directory) {
85
+ const files = (await getAllFiles()).map(file => normalizePath(file.path));
86
+ if (directory) {
87
+ directory = normalizePath(directory);
88
+ return files.filter(file => file.startsWith(directory));
89
+ }
90
+ return files;
91
+ }
92
+ exports.listFilesAsync = listFilesAsync;
93
+ async function downloadTranslationsAsync(directory) {
94
+ const { translationsApi } = getClient();
95
+ let buildId;
96
+ let status;
97
+ const options = {
98
+ skipUntranslatedFiles: true,
99
+ exportApprovedOnly: true
100
+ };
101
+ if (directory) {
102
+ pxt.log(`Building translations for directory ${directory}`);
103
+ const directoryId = await getDirectoryIdAsync(directory);
104
+ const buildResp = await translationsApi.buildProjectDirectoryTranslation(projectId, directoryId, options);
105
+ buildId = buildResp.data.id;
106
+ status = buildResp.data.status;
107
+ }
108
+ else {
109
+ pxt.log(`Building all translations`);
110
+ const buildResp = await translationsApi.buildProject(projectId, options);
111
+ buildId = buildResp.data.id;
112
+ status = buildResp.data.status;
113
+ }
114
+ // Translation builds take a long time, so poll for progress
115
+ while (status !== "finished") {
116
+ const progress = await translationsApi.checkBuildStatus(projectId, buildId);
117
+ status = progress.data.status;
118
+ pxt.log(`Translation build progress: ${progress.data.progress}%`);
119
+ if (status !== "finished") {
120
+ await pxt.Util.delay(5000);
121
+ }
122
+ }
123
+ pxt.log("Fetching translation build");
124
+ const downloadReq = await translationsApi.downloadTranslations(projectId, buildId);
125
+ // The downloaded file is a zip of all files broken out in a directory for each language
126
+ // e.g. /en/docs/tutorial.md, /fr/docs/tutorial.md, etc.
127
+ pxt.log("Downloading translation zip");
128
+ const zipFile = await axios_1.default.get(downloadReq.data.url, { responseType: 'arraybuffer' });
129
+ const zip = new AdmZip(Buffer.from(zipFile.data));
130
+ const entries = zip.getEntries();
131
+ const filesystem = {};
132
+ for (const entry of entries) {
133
+ if (entry.isDirectory)
134
+ continue;
135
+ filesystem[entry.entryName] = zip.readAsText(entry);
136
+ }
137
+ pxt.log("Translation download complete");
138
+ return filesystem;
139
+ }
140
+ exports.downloadTranslationsAsync = downloadTranslationsAsync;
141
+ async function downloadFileTranslationsAsync(fileName) {
142
+ const { translationsApi } = getClient();
143
+ const fileId = await getFileIdAsync(fileName);
144
+ const projectInfo = await getProjectInfoAsync();
145
+ let todo = projectInfo.targetLanguageIds.filter(id => id !== "en");
146
+ if (pxt.appTarget && pxt.appTarget.appTheme && pxt.appTarget.appTheme.availableLocales) {
147
+ todo = todo.filter(l => pxt.appTarget.appTheme.availableLocales.indexOf(l) > -1);
148
+ }
149
+ const options = {
150
+ skipUntranslatedFiles: true,
151
+ exportApprovedOnly: true
152
+ };
153
+ const results = {};
154
+ // There's no API to get all translations for a file, so we have to build each one individually
155
+ for (const language of todo) {
156
+ pxt.debug(`Building ${language} translation for '${fileName}'`);
157
+ try {
158
+ const buildResp = await translationsApi.buildProjectFileTranslation(projectId, fileId, Object.assign({ targetLanguageId: language }, options));
159
+ if (!buildResp.data) {
160
+ pxt.debug(`No translation available for ${language}`);
161
+ continue;
162
+ }
163
+ const textResp = await axios_1.default.get(buildResp.data.url, { responseType: "text" });
164
+ results[language] = textResp.data;
165
+ }
166
+ catch (e) {
167
+ console.log(`Error building ${language} translation for '${fileName}'`, e);
168
+ continue;
169
+ }
170
+ }
171
+ return results;
172
+ }
173
+ exports.downloadFileTranslationsAsync = downloadFileTranslationsAsync;
174
+ async function getFileIdAsync(fileName) {
175
+ for (const file of await getAllFiles()) {
176
+ if (normalizePath(file.path) === normalizePath(fileName)) {
177
+ return file.id;
178
+ }
179
+ }
180
+ throw new Error(`File '${fileName}' not found in crowdin project`);
181
+ }
182
+ async function getDirectoryIdAsync(dirName) {
183
+ for (const dir of await getAllDirectories()) {
184
+ if (normalizePath(dir.path) === normalizePath(dirName)) {
185
+ return dir.id;
186
+ }
187
+ }
188
+ throw new Error(`Directory '${dirName}' not found in crowdin project`);
189
+ }
190
+ async function mkdirAsync(dirName) {
191
+ const dirs = await getAllDirectories();
192
+ for (const dir of dirs) {
193
+ if (normalizePath(dir.path) === normalizePath(dirName)) {
194
+ return dir;
195
+ }
196
+ }
197
+ let parentDirId;
198
+ const parentDir = path.dirname(dirName);
199
+ if (parentDir && parentDir !== ".") {
200
+ parentDirId = (await mkdirAsync(parentDir)).id;
201
+ }
202
+ return await createDirectory(path.basename(dirName), parentDirId);
203
+ }
204
+ async function getAllDirectories() {
205
+ // This request takes a decent amount of time, so cache the results
206
+ if (!fetchedDirectories) {
207
+ const { sourceFilesApi } = getClient();
208
+ pxt.debug(`Fetching directories`);
209
+ const dirsResponse = await sourceFilesApi
210
+ .withFetchAll()
211
+ .listProjectDirectories(projectId, {});
212
+ let dirs = dirsResponse.data.map(fileResponse => fileResponse.data);
213
+ if (!dirs.length) {
214
+ throw new Error("No directories found!");
215
+ }
216
+ pxt.debug(`Directory count: ${dirs.length}`);
217
+ fetchedDirectories = dirs;
218
+ }
219
+ return fetchedDirectories;
220
+ }
221
+ async function getAllFiles() {
222
+ // This request takes a decent amount of time, so cache the results
223
+ if (!fetchedFiles) {
224
+ const { sourceFilesApi } = getClient();
225
+ pxt.debug(`Fetching files`);
226
+ const filesResponse = await sourceFilesApi
227
+ .withFetchAll()
228
+ .listProjectFiles(projectId, {});
229
+ let files = filesResponse.data.map(fileResponse => fileResponse.data);
230
+ if (!files.length) {
231
+ throw new Error("No files found!");
232
+ }
233
+ pxt.debug(`File count: ${files.length}`);
234
+ fetchedFiles = files;
235
+ }
236
+ return fetchedFiles;
237
+ }
238
+ async function createFile(fileName, fileContent, directoryId) {
239
+ if (pxt.crowdin.testMode)
240
+ return;
241
+ const { uploadStorageApi, sourceFilesApi } = getClient();
242
+ // This request happens in two parts: first we upload the file to the storage API,
243
+ // then we actually create the file
244
+ const storageResponse = await uploadStorageApi.addStorage(fileName, fileContent);
245
+ const file = await sourceFilesApi.createFile(projectId, {
246
+ storageId: storageResponse.data.id,
247
+ name: fileName,
248
+ directoryId
249
+ });
250
+ // Make sure to add the file to the cache if it exists
251
+ if (fetchedFiles) {
252
+ fetchedFiles.push(file.data);
253
+ }
254
+ }
255
+ async function createDirectory(dirName, directoryId) {
256
+ if (pxt.crowdin.testMode)
257
+ return undefined;
258
+ const { sourceFilesApi } = getClient();
259
+ const dir = await sourceFilesApi.createDirectory(projectId, {
260
+ name: dirName,
261
+ directoryId
262
+ });
263
+ // Make sure to add the directory to the cache if it exists
264
+ if (fetchedDirectories) {
265
+ fetchedDirectories.push(dir.data);
266
+ }
267
+ return dir.data;
268
+ }
269
+ async function restoreFileBefore(filename, cutoffTime) {
270
+ const revisions = await listFileRevisions(filename);
271
+ let lastRevision;
272
+ let lastRevisionBeforeCutoff;
273
+ for (const rev of revisions) {
274
+ const time = new Date(rev.date).getTime();
275
+ if (lastRevision) {
276
+ if (time > new Date(lastRevision.date).getTime()) {
277
+ lastRevision = rev;
278
+ }
279
+ }
280
+ else {
281
+ lastRevision = rev;
282
+ }
283
+ if (time < cutoffTime) {
284
+ if (lastRevisionBeforeCutoff) {
285
+ if (time > new Date(lastRevisionBeforeCutoff.date).getTime()) {
286
+ lastRevisionBeforeCutoff = rev;
287
+ }
288
+ }
289
+ else {
290
+ lastRevisionBeforeCutoff = rev;
291
+ }
292
+ }
293
+ }
294
+ if (lastRevision === lastRevisionBeforeCutoff) {
295
+ pxt.log(`${filename} already at most recent valid revision before ${formatTime(cutoffTime)}`);
296
+ }
297
+ else if (lastRevisionBeforeCutoff) {
298
+ pxt.log(`Restoring ${filename} to revision ${formatTime(new Date(lastRevisionBeforeCutoff.date).getTime())}`);
299
+ await restorefile(lastRevisionBeforeCutoff.fileId, lastRevisionBeforeCutoff.id);
300
+ }
301
+ else {
302
+ pxt.log(`No revisions found for ${filename} before ${formatTime(cutoffTime)}`);
303
+ }
304
+ }
305
+ exports.restoreFileBefore = restoreFileBefore;
306
+ function formatTime(time) {
307
+ const date = new Date(time);
308
+ return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
309
+ }
310
+ async function listFileRevisions(filename) {
311
+ const { sourceFilesApi } = getClient();
312
+ const fileId = await getFileIdAsync(filename);
313
+ const revisions = await sourceFilesApi
314
+ .withFetchAll()
315
+ .listFileRevisions(projectId, fileId);
316
+ return revisions.data.map(rev => rev.data);
317
+ }
318
+ async function updateFile(fileId, fileName, fileContent) {
319
+ if (pxt.crowdin.testMode)
320
+ return;
321
+ const { uploadStorageApi, sourceFilesApi } = getClient();
322
+ const storageResponse = await uploadStorageApi.addStorage(fileName, fileContent);
323
+ await sourceFilesApi.updateOrRestoreFile(projectId, fileId, {
324
+ storageId: storageResponse.data.id,
325
+ updateOption: "keep_translations"
326
+ });
327
+ }
328
+ async function restorefile(fileId, revisionId) {
329
+ if (pxt.crowdin.testMode)
330
+ return;
331
+ const { sourceFilesApi } = getClient();
332
+ await sourceFilesApi.updateOrRestoreFile(projectId, fileId, {
333
+ revisionId
334
+ });
335
+ }
336
+ function getClient() {
337
+ if (!client) {
338
+ const crowdinConfig = {
339
+ retryConfig: {
340
+ retries: 5,
341
+ waitInterval: 5000,
342
+ conditions: [
343
+ {
344
+ test: (error) => {
345
+ // do not retry when result has not changed
346
+ return (error === null || error === void 0 ? void 0 : error.code) == 304;
347
+ }
348
+ }
349
+ ]
350
+ }
351
+ };
352
+ client = new crowdin_api_client_1.default(crowdinCredentials(), crowdinConfig);
353
+ }
354
+ return client;
355
+ }
356
+ function crowdinCredentials() {
357
+ var _a, _b;
358
+ const token = process.env[pxt.crowdin.KEY_VARIABLE];
359
+ if (!token) {
360
+ throw new Error(`Crowdin token not found in environment variable ${pxt.crowdin.KEY_VARIABLE}`);
361
+ }
362
+ if (((_b = (_a = pxt.appTarget) === null || _a === void 0 ? void 0 : _a.appTheme) === null || _b === void 0 ? void 0 : _b.crowdinProjectId) !== undefined) {
363
+ setProjectId(pxt.appTarget.appTheme.crowdinProjectId);
364
+ }
365
+ return { token };
366
+ }
367
+ // calls path.normalize and removes leading slash
368
+ function normalizePath(p) {
369
+ p = path.normalize(p);
370
+ p = p.replace(/\\/g, "/");
371
+ if (/^[\/\\]/.test(p))
372
+ p = p.slice(1);
373
+ return p;
374
+ }