pxt-core
Version:
Microsoft MakeCode provides Blocks / JavaScript / Python tools and editors
375 lines (374 loc) • 14.6 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.restoreFileBefore = exports.downloadFileTranslationsAsync = exports.downloadTranslationsAsync = exports.listFilesAsync = exports.getFileProgressAsync = exports.getDirectoryProgressAsync = exports.getProjectProgressAsync = exports.getProjectInfoAsync = exports.uploadFileAsync = exports.setProjectId = void 0;
const crowdin_api_client_1 = require("@crowdin/crowdin-api-client");
const path = require("path");
const axios_1 = require("axios");
const AdmZip = require("adm-zip");
let client;
const KINDSCRIPT_PROJECT_ID = 157956;
let projectId = KINDSCRIPT_PROJECT_ID;
let fetchedFiles;
let fetchedDirectories;
function setProjectId(id) {
projectId = id;
fetchedFiles = undefined;
fetchedDirectories = undefined;
}
exports.setProjectId = setProjectId;
async function uploadFileAsync(fileName, fileContent) {
if (pxt.crowdin.testMode)
return;
const files = await getAllFiles();
// If file already exists, update it
for (const file of files) {
if (normalizePath(file.path) === normalizePath(fileName)) {
await updateFile(file.id, path.basename(fileName), fileContent);
return;
}
}
// Ensure directory exists
const parentDir = path.dirname(fileName);
let parentDirId;
if (parentDir && parentDir !== ".") {
parentDirId = (await mkdirAsync(parentDir)).id;
}
// Create new file
await createFile(path.basename(fileName), fileContent, parentDirId);
}
exports.uploadFileAsync = uploadFileAsync;
async function getProjectInfoAsync() {
const { projectsGroupsApi } = getClient();
const project = await projectsGroupsApi.getProject(projectId);
return project.data;
}
exports.getProjectInfoAsync = getProjectInfoAsync;
async function getProjectProgressAsync(languages) {
const { translationStatusApi } = getClient();
const stats = await translationStatusApi
.withFetchAll()
.getProjectProgress(projectId);
let results = stats.data.map(stat => stat.data);
if (languages) {
results = results.filter(stat => languages.indexOf(stat.language.locale) !== -1 || languages.indexOf(stat.language.twoLettersCode) !== -1);
}
return results;
}
exports.getProjectProgressAsync = getProjectProgressAsync;
async function getDirectoryProgressAsync(directory, languages) {
const { translationStatusApi } = getClient();
const directoryId = await getDirectoryIdAsync(directory);
const stats = await translationStatusApi
.withFetchAll()
.getDirectoryProgress(projectId, directoryId);
let results = stats.data.map(stat => stat.data);
if (languages) {
results = results.filter(stat => languages.indexOf(stat.language.locale) !== -1 || languages.indexOf(stat.language.twoLettersCode) !== -1);
}
return results;
}
exports.getDirectoryProgressAsync = getDirectoryProgressAsync;
async function getFileProgressAsync(file, languages) {
const { translationStatusApi } = getClient();
const fileId = await getFileIdAsync(file);
const stats = await translationStatusApi
.withFetchAll()
.getFileProgress(projectId, fileId);
let results = stats.data.map(stat => stat.data);
if (languages) {
results = results.filter(stat => languages.indexOf(stat.language.locale) !== -1 || languages.indexOf(stat.language.twoLettersCode) !== -1);
}
return results;
}
exports.getFileProgressAsync = getFileProgressAsync;
async function listFilesAsync(directory) {
const files = (await getAllFiles()).map(file => normalizePath(file.path));
if (directory) {
directory = normalizePath(directory);
return files.filter(file => file.startsWith(directory));
}
return files;
}
exports.listFilesAsync = listFilesAsync;
async function downloadTranslationsAsync(directory) {
const { translationsApi } = getClient();
let buildId;
let status;
const options = {
skipUntranslatedFiles: true,
exportApprovedOnly: true
};
if (directory) {
pxt.log(`Building translations for directory ${directory}`);
const directoryId = await getDirectoryIdAsync(directory);
const buildResp = await translationsApi.buildProjectDirectoryTranslation(projectId, directoryId, options);
buildId = buildResp.data.id;
status = buildResp.data.status;
}
else {
pxt.log(`Building all translations`);
const buildResp = await translationsApi.buildProject(projectId, options);
buildId = buildResp.data.id;
status = buildResp.data.status;
}
// Translation builds take a long time, so poll for progress
while (status !== "finished") {
const progress = await translationsApi.checkBuildStatus(projectId, buildId);
status = progress.data.status;
pxt.log(`Translation build progress: ${progress.data.progress}%`);
if (status !== "finished") {
await pxt.Util.delay(5000);
}
}
pxt.log("Fetching translation build");
const downloadReq = await translationsApi.downloadTranslations(projectId, buildId);
// The downloaded file is a zip of all files broken out in a directory for each language
// e.g. /en/docs/tutorial.md, /fr/docs/tutorial.md, etc.
pxt.log("Downloading translation zip");
const zipFile = await axios_1.default.get(downloadReq.data.url, { responseType: 'arraybuffer' });
const zip = new AdmZip(Buffer.from(zipFile.data));
const entries = zip.getEntries();
const filesystem = {};
for (const entry of entries) {
if (entry.isDirectory)
continue;
filesystem[entry.entryName] = zip.readAsText(entry);
}
pxt.log("Translation download complete");
return filesystem;
}
exports.downloadTranslationsAsync = downloadTranslationsAsync;
async function downloadFileTranslationsAsync(fileName) {
const { translationsApi } = getClient();
const fileId = await getFileIdAsync(fileName);
const projectInfo = await getProjectInfoAsync();
let todo = projectInfo.targetLanguageIds.filter(id => id !== "en");
if (pxt.appTarget && pxt.appTarget.appTheme && pxt.appTarget.appTheme.availableLocales) {
todo = todo.filter(l => pxt.appTarget.appTheme.availableLocales.indexOf(l) > -1);
}
const options = {
skipUntranslatedFiles: true,
exportApprovedOnly: true
};
const results = {};
// There's no API to get all translations for a file, so we have to build each one individually
for (const language of todo) {
pxt.debug(`Building ${language} translation for '${fileName}'`);
try {
const buildResp = await translationsApi.buildProjectFileTranslation(projectId, fileId, Object.assign({ targetLanguageId: language }, options));
if (!buildResp.data) {
pxt.debug(`No translation available for ${language}`);
continue;
}
const textResp = await axios_1.default.get(buildResp.data.url, { responseType: "text" });
results[language] = textResp.data;
}
catch (e) {
console.log(`Error building ${language} translation for '${fileName}'`, e);
continue;
}
}
return results;
}
exports.downloadFileTranslationsAsync = downloadFileTranslationsAsync;
async function getFileIdAsync(fileName) {
for (const file of await getAllFiles()) {
if (normalizePath(file.path) === normalizePath(fileName)) {
return file.id;
}
}
throw new Error(`File '${fileName}' not found in crowdin project`);
}
async function getDirectoryIdAsync(dirName) {
for (const dir of await getAllDirectories()) {
if (normalizePath(dir.path) === normalizePath(dirName)) {
return dir.id;
}
}
throw new Error(`Directory '${dirName}' not found in crowdin project`);
}
async function mkdirAsync(dirName) {
const dirs = await getAllDirectories();
for (const dir of dirs) {
if (normalizePath(dir.path) === normalizePath(dirName)) {
return dir;
}
}
let parentDirId;
const parentDir = path.dirname(dirName);
if (parentDir && parentDir !== ".") {
parentDirId = (await mkdirAsync(parentDir)).id;
}
return await createDirectory(path.basename(dirName), parentDirId);
}
async function getAllDirectories() {
// This request takes a decent amount of time, so cache the results
if (!fetchedDirectories) {
const { sourceFilesApi } = getClient();
pxt.debug(`Fetching directories`);
const dirsResponse = await sourceFilesApi
.withFetchAll()
.listProjectDirectories(projectId, {});
let dirs = dirsResponse.data.map(fileResponse => fileResponse.data);
if (!dirs.length) {
throw new Error("No directories found!");
}
pxt.debug(`Directory count: ${dirs.length}`);
fetchedDirectories = dirs;
}
return fetchedDirectories;
}
async function getAllFiles() {
// This request takes a decent amount of time, so cache the results
if (!fetchedFiles) {
const { sourceFilesApi } = getClient();
pxt.debug(`Fetching files`);
const filesResponse = await sourceFilesApi
.withFetchAll()
.listProjectFiles(projectId, {});
let files = filesResponse.data.map(fileResponse => fileResponse.data);
if (!files.length) {
throw new Error("No files found!");
}
pxt.debug(`File count: ${files.length}`);
fetchedFiles = files;
}
return fetchedFiles;
}
async function createFile(fileName, fileContent, directoryId) {
if (pxt.crowdin.testMode)
return;
const { uploadStorageApi, sourceFilesApi } = getClient();
// This request happens in two parts: first we upload the file to the storage API,
// then we actually create the file
const storageResponse = await uploadStorageApi.addStorage(fileName, fileContent);
const file = await sourceFilesApi.createFile(projectId, {
storageId: storageResponse.data.id,
name: fileName,
directoryId
});
// Make sure to add the file to the cache if it exists
if (fetchedFiles) {
fetchedFiles.push(file.data);
}
}
async function createDirectory(dirName, directoryId) {
if (pxt.crowdin.testMode)
return undefined;
const { sourceFilesApi } = getClient();
const dir = await sourceFilesApi.createDirectory(projectId, {
name: dirName,
directoryId
});
// Make sure to add the directory to the cache if it exists
if (fetchedDirectories) {
fetchedDirectories.push(dir.data);
}
return dir.data;
}
async function restoreFileBefore(filename, cutoffTime) {
const revisions = await listFileRevisions(filename);
let lastRevision;
let lastRevisionBeforeCutoff;
for (const rev of revisions) {
const time = new Date(rev.date).getTime();
if (lastRevision) {
if (time > new Date(lastRevision.date).getTime()) {
lastRevision = rev;
}
}
else {
lastRevision = rev;
}
if (time < cutoffTime) {
if (lastRevisionBeforeCutoff) {
if (time > new Date(lastRevisionBeforeCutoff.date).getTime()) {
lastRevisionBeforeCutoff = rev;
}
}
else {
lastRevisionBeforeCutoff = rev;
}
}
}
if (lastRevision === lastRevisionBeforeCutoff) {
pxt.log(`${filename} already at most recent valid revision before ${formatTime(cutoffTime)}`);
}
else if (lastRevisionBeforeCutoff) {
pxt.log(`Restoring ${filename} to revision ${formatTime(new Date(lastRevisionBeforeCutoff.date).getTime())}`);
await restorefile(lastRevisionBeforeCutoff.fileId, lastRevisionBeforeCutoff.id);
}
else {
pxt.log(`No revisions found for ${filename} before ${formatTime(cutoffTime)}`);
}
}
exports.restoreFileBefore = restoreFileBefore;
function formatTime(time) {
const date = new Date(time);
return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
}
async function listFileRevisions(filename) {
const { sourceFilesApi } = getClient();
const fileId = await getFileIdAsync(filename);
const revisions = await sourceFilesApi
.withFetchAll()
.listFileRevisions(projectId, fileId);
return revisions.data.map(rev => rev.data);
}
async function updateFile(fileId, fileName, fileContent) {
if (pxt.crowdin.testMode)
return;
const { uploadStorageApi, sourceFilesApi } = getClient();
const storageResponse = await uploadStorageApi.addStorage(fileName, fileContent);
await sourceFilesApi.updateOrRestoreFile(projectId, fileId, {
storageId: storageResponse.data.id,
updateOption: "keep_translations"
});
}
async function restorefile(fileId, revisionId) {
if (pxt.crowdin.testMode)
return;
const { sourceFilesApi } = getClient();
await sourceFilesApi.updateOrRestoreFile(projectId, fileId, {
revisionId
});
}
function getClient() {
if (!client) {
const crowdinConfig = {
retryConfig: {
retries: 5,
waitInterval: 5000,
conditions: [
{
test: (error) => {
// do not retry when result has not changed
return (error === null || error === void 0 ? void 0 : error.code) == 304;
}
}
]
}
};
client = new crowdin_api_client_1.default(crowdinCredentials(), crowdinConfig);
}
return client;
}
function crowdinCredentials() {
var _a, _b;
const token = process.env[pxt.crowdin.KEY_VARIABLE];
if (!token) {
throw new Error(`Crowdin token not found in environment variable ${pxt.crowdin.KEY_VARIABLE}`);
}
if (((_b = (_a = pxt.appTarget) === null || _a === void 0 ? void 0 : _a.appTheme) === null || _b === void 0 ? void 0 : _b.crowdinProjectId) !== undefined) {
setProjectId(pxt.appTarget.appTheme.crowdinProjectId);
}
return { token };
}
// calls path.normalize and removes leading slash
function normalizePath(p) {
p = path.normalize(p);
p = p.replace(/\\/g, "/");
if (/^[\/\\]/.test(p))
p = p.slice(1);
return p;
}
;