Skip to content

Commit 1c7e217

Browse files
committed
Add script to download translations from transifex
1 parent 5c958bc commit 1c7e217

File tree

3 files changed

+204
-2
lines changed

3 files changed

+204
-2
lines changed

Diff for: package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,15 @@
3535
},
3636
"scripts": {
3737
"prepare": "cross-env THEIA_ELECTRON_SKIP_REPLACE_FFMPEG=1 lerna run prepare && yarn download:plugins",
38-
"cleanup": "npx rimraf ./**/node_modules && rm -rf ./node_modules ./.browser_modules ./arduino-ide-extension/build ./arduino-ide-extension/downloads ./arduino-ide-extension/Examples ./arduino-ide-extension/lib ./browser-app/lib ./browser-app/src-gen ./browser-app/gen-webpack.config.js ./electron-app/lib ./electron-app/src-gen ./electron-app/gen-webpack.config.js",
38+
"cleanup": "npx rimraf ./**/node_modules && rm -rf ./node_modules ./.browser_modules ./arduino-ide-extension/build ./arduino-ide-extension/downloads ./arduino-ide-extension/Examples ./arduino-ide-extension/lib ./browser-app/lib ./browser-app/src-gen ./browser-app/gen-webpack.config.js ./electron-app/lib ./electron-app/src-gen ./electron-app/gen-webpack.config.js",
3939
"rebuild:browser": "theia rebuild:browser",
4040
"rebuild:electron": "theia rebuild:electron",
4141
"start": "yarn --cwd ./electron-app start",
4242
"watch": "lerna run watch --parallel",
4343
"test": "lerna run test",
4444
"download:plugins": "theia download:plugins",
45-
"update:version": "node ./scripts/update-version.js"
45+
"update:version": "node ./scripts/update-version.js",
46+
"i18n:pull": "node ./scripts/i18n/transifex-pull.js ./i18n/"
4647
},
4748
"lint-staged": {
4849
"./arduino-ide-extension/**/*.{ts,tsx}": [

Diff for: scripts/i18n/transifex-pull.js

+149
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// @ts-check
2+
3+
const transifex = require('./transifex');
4+
const util = require('util');
5+
const shell = require('shelljs');
6+
const fetch = require('node-fetch');
7+
const download = require('download');
8+
9+
const getLanguages = async (organization, project) => {
10+
const url = transifex.url(
11+
util.format('projects/o:%s:p:%s/languages', organization, project)
12+
);
13+
const json = await fetch(url, { headers: transifex.authHeader() })
14+
.catch(err => {
15+
shell.echo(err);
16+
shell.exit(1);
17+
})
18+
.then(res => res.json());
19+
let languages = [];
20+
json['data'].forEach(e => {
21+
const languageCode = e['attributes']['code'];
22+
// Skip english since it's the one we generate
23+
if (languageCode === 'en') {
24+
return;
25+
}
26+
languages.push(languageCode);
27+
});
28+
return languages;
29+
};
30+
31+
const requestTranslationDownload = async (relationships) => {
32+
let url = transifex.url('resource_translations_async_downloads');
33+
const data = {
34+
data: {
35+
relationships,
36+
type: 'resource_translations_async_downloads'
37+
}
38+
};
39+
const headers = transifex.authHeader();
40+
headers['Content-Type'] = 'application/vnd.api+json';
41+
const json = await fetch(url, {
42+
method: 'POST',
43+
headers,
44+
body: JSON.stringify(data)
45+
})
46+
.catch(err => {
47+
shell.echo(err);
48+
shell.exit(1);
49+
})
50+
.then(res => res.json());
51+
52+
return json['data']['id'];
53+
};
54+
55+
const getTranslationDownloadStatus = async (language, downloadRequestId) => {
56+
// The download request status must be asked from time to time, if it's
57+
// still pending we try again using exponentional backoff starting from 2.5 seconds.
58+
let backoffMs = 2500;
59+
while (true) {
60+
const url = transifex.url(
61+
util.format('resource_translations_async_downloads/%s', downloadRequestId)
62+
);
63+
const options = {
64+
headers: transifex.authHeader(),
65+
redirect: 'manual'
66+
};
67+
const res = await fetch(url, options).catch(err => {
68+
shell.echo(err);
69+
shell.exit(1);
70+
});
71+
72+
if (res.status === 303) {
73+
// When the file to download is ready we get redirected
74+
return {
75+
language,
76+
downloadUrl: res.headers.get('location')
77+
};
78+
}
79+
80+
const json = await res.json();
81+
const downloadStatus = json['data']['attributes']['status'];
82+
if (downloadStatus == 'pending' || downloadStatus == 'processing') {
83+
await new Promise(r => setTimeout(r, backoffMs));
84+
backoffMs = backoffMs * 2;
85+
// Retry the download request status again
86+
continue;
87+
} else if (downloadStatus == 'failed') {
88+
const errors = [];
89+
json['data']['attributes']['errors'].forEach(err => {
90+
errors.push(util.format('%s: %s', err.code, err.details));
91+
});
92+
throw util.format('Download request failed: %s', errors.join(', '));
93+
}
94+
throw 'Download request failed in an unforeseen way';
95+
}
96+
};
97+
98+
(async () => {
99+
const { organization, project, resource } = await transifex.credentials();
100+
const translationsDirectory = process.argv[2];
101+
if (!translationsDirectory) {
102+
shell.echo('Traslations directory not specified');
103+
shell.exit(1);
104+
}
105+
106+
const languages = await getLanguages(organization, project);
107+
shell.echo('translations found:', languages.join(', '));
108+
109+
let downloadIds = [];
110+
for (const language of languages) {
111+
downloadIds.push({
112+
language,
113+
id: await requestTranslationDownload({
114+
language: {
115+
data: {
116+
id: util.format('l:%s', language),
117+
type: 'languages'
118+
}
119+
},
120+
resource: {
121+
data: {
122+
id: util.format('o:%s:p:%s:r:%s', organization, project, resource),
123+
type: 'resources'
124+
}
125+
}
126+
})
127+
});
128+
}
129+
130+
const res = await Promise.all(
131+
downloadIds.map(d => getTranslationDownloadStatus(d['language'], d['id']))
132+
).catch(err => {
133+
shell.echo(err);
134+
shell.exit(1);
135+
});
136+
137+
await Promise.all(
138+
res.map(r => {
139+
return download(r['downloadUrl'], translationsDirectory, {
140+
filename: r['language'] + '.json'
141+
});
142+
})
143+
).catch(err => {
144+
shell.echo(err);
145+
shell.exit(1);
146+
});
147+
148+
shell.echo('Translation files downloaded.');
149+
})();

Diff for: scripts/i18n/transifex.js

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// @ts-check
2+
3+
const shell = require('shelljs');
4+
const util = require('util');
5+
6+
const TRANSIFEX_ENDPOINT = 'https://rest.api.transifex.com/';
7+
8+
const apiKey = () => {
9+
const apiKey = process.env.TRANSIFEX_API_KEY;
10+
if (apiKey === '') {
11+
shell.echo('missing TRANSIFEX_API_KEY environment variable');
12+
shell.exit(1)
13+
}
14+
return apiKey
15+
}
16+
17+
exports.credentials = async () => {
18+
const organization = process.env.TRANSIFEX_ORGANIZATION;
19+
const project = process.env.TRANSIFEX_PROJECT;
20+
const resource = process.env.TRANSIFEX_RESOURCE;
21+
22+
if (organization === '') {
23+
shell.echo('missing TRANSIFEX_ORGANIZATION environment variable');
24+
shell.exit(1)
25+
}
26+
27+
if (project === '') {
28+
shell.echo('missing TRANSIFEX_PROJECT environment variable');
29+
shell.exit(1)
30+
}
31+
32+
if (resource === '') {
33+
shell.echo('missing TRANSIFEX_RESOURCE environment variable');
34+
shell.exit(1)
35+
}
36+
37+
return { organization, project, resource }
38+
}
39+
40+
exports.url = (path, queryParameters) => {
41+
let url = util.format('%s%s', TRANSIFEX_ENDPOINT, path);
42+
if (queryParameters) {
43+
url = util.format('%s?%s', url, queryParameters);
44+
}
45+
return url
46+
}
47+
48+
exports.authHeader = () => {
49+
return {
50+
'Authorization': util.format("Bearer %s", apiKey()),
51+
}
52+
}

0 commit comments

Comments
 (0)