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.
- package/built/cli.js +56 -45
- package/built/crowdin.d.ts +2 -1
- package/built/crowdin.js +218 -213
- package/built/crowdinApi.d.ts +10 -0
- package/built/crowdinApi.js +374 -0
- package/built/pxt.js +75 -296
- package/built/pxtblockly.js +8 -6
- package/built/pxtblocks.d.ts +4 -0
- package/built/pxtblocks.js +8 -6
- package/built/pxtlib.d.ts +3 -37
- package/built/pxtlib.js +19 -251
- package/built/target.js +1 -1
- package/built/web/main.js +1 -1
- package/built/web/pxtapp.js +1 -1
- package/built/web/pxtasseteditor.js +1 -1
- package/built/web/pxtblockly.js +1 -1
- package/built/web/pxtblocks.js +1 -1
- package/built/web/pxtembed.js +2 -2
- package/built/web/pxtlib.js +1 -1
- package/built/web/pxtworker.js +1 -1
- package/localtypings/pxtarget.d.ts +1 -0
- package/package.json +5 -1
|
@@ -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
|
+
}
|