Skip to content

Commit deea666

Browse files
committed
upgrade transifex to API v3
see also openstreetmap/iD#9375
1 parent 4402f14 commit deea666

File tree

2 files changed

+78
-80
lines changed

2 files changed

+78
-80
lines changed

lib/translations.js

Lines changed: 76 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,29 @@
11
/* Downloads the latest translations from Transifex */
22
import fs from 'fs';
33
import fetch from 'node-fetch';
4-
import btoa from 'btoa';
54
import YAML from 'js-yaml';
5+
import { transifexApi } from '@transifex/api';
66

77

88
function fetchTranslations(options) {
99

10-
let defaultCredentials = {
11-
user: 'api',
12-
password: ''
13-
};
14-
if (fs.existsSync(`${process.cwd()}/transifex.auth`)) {
15-
// Credentials can be stored in transifex.auth as a json object. You should probably gitignore this file.
16-
// You can use an API key instead of your password: https://docs.transifex.com/api/introduction#authentication
10+
// Transifex doesn't allow anonymous downloading
11+
/* eslint-disable no-process-env */
12+
if (process.env.transifex_password) {
13+
// Deployment scripts may prefer environment variables
14+
transifexApi.setup({ auth: process.env.transifex_password });
15+
} else {
16+
// Credentials can be stored in transifex.auth as a json object. This file is gitignored.
17+
// You must use an API token for authentication: You can generate one at https://www.transifex.com/user/settings/api/.
1718
// {
18-
// "user": "username",
19-
// "password": "password"
19+
// "password": "<api_key>"
2020
// }
21-
defaultCredentials = JSON.parse(fs.readFileSync(`${process.cwd()}/transifex.auth`, 'utf8'));
21+
transifexApi.setup({ auth: JSON.parse(fs.readFileSync('./transifex.auth', 'utf8')).password });
2222
}
23+
/* eslint-enable no-process-env */
2324

2425
if (!options) options = {};
2526
options = Object.assign({
26-
translCredentials: defaultCredentials,
2727
translOrgId: '',
2828
translProjectId: '',
2929
translResourceIds: ['presets'],
@@ -38,15 +38,6 @@ function fetchTranslations(options) {
3838
fs.mkdirSync(outDir);
3939
}
4040

41-
const fetchOpts = {
42-
headers: {
43-
'Authorization': 'Basic ' + btoa(options.translCredentials.user + ':' + options.translCredentials.password),
44-
}
45-
};
46-
47-
const apiroot = 'https://www.transifex.com/api/2';
48-
const projectURL = `${apiroot}/project/${options.translProjectId}`;
49-
5041
const translResourceIds = options.translResourceIds;
5142
return new Promise(function(resolve) {
5243
asyncMap(translResourceIds, getResourceInfo, function(err, results) {
@@ -59,37 +50,41 @@ function fetchTranslations(options) {
5950
});
6051

6152

62-
function getResourceInfo(resourceId, callback) {
63-
let url = `https://api.transifex.com/organizations/${options.translOrgId}/projects/${options.translProjectId}/resources/${resourceId}`;
64-
fetch(url, fetchOpts)
65-
.then(res => {
66-
process.stdout.write(`${res.status}: ${url}\n`);
67-
return res.json();
68-
})
69-
.then(json => {
70-
callback(null, json);
71-
})
72-
.catch(err => callback(err));
53+
async function getResourceInfo(resourceId, callback) {
54+
try {
55+
const result = [];
56+
for await (const stat of transifexApi.ResourceLanguageStats.filter({
57+
project: `o:${options.translOrgId}:p:${options.translProjectId}`,
58+
resource: `o:${options.translOrgId}:p:${options.translProjectId}:r:${resourceId}`
59+
}).all()) {
60+
result.push(stat);
61+
}
62+
console.log(`got resource language stats collection for ${resourceId}`);
63+
callback(null, result);
64+
} catch (err) {
65+
console.error(`error while getting resource language stats collection for ${resourceId}`, err);
66+
callback(err);
67+
}
7368
}
7469

7570
function gotResourceInfo(err, results) {
7671
if (err) return process.stderr.write(err + '\n');
7772

7873
let coverageByLocaleCode = {};
7974
results.forEach(function(info) {
80-
for (let code in info.stats) {
81-
let type = 'translated';
82-
if (options.translReviewedOnly &&
83-
(!Array.isArray(options.translReviewedOnly) || options.translReviewedOnly.indexOf(code) !== -1)) {
84-
// reviewed_1 = reviewed, reviewed_2 = proofread
85-
type = 'reviewed_1';
75+
info.forEach(stat => {
76+
let code = stat.relationships.language.data.id.substr(2).replace(/_/g, '-');
77+
let type = 'translated_strings';
78+
if (options.translReviewedOnly && (
79+
!Array.isArray(options.translReviewedOnly)
80+
|| options.translReviewedOnly.indexOf(code) !== -1)) {
81+
type = 'reviewed_strings';
8682
}
87-
let coveragePart = info.stats[code][type].percentage / results.length;
83+
let coveragePart = (stat.attributes[type] / stat.attributes.total_strings) / results.length;
8884

89-
code = code.replace(/_/g, '-');
9085
if (coverageByLocaleCode[code] === undefined) coverageByLocaleCode[code] = 0;
9186
coverageByLocaleCode[code] += coveragePart;
92-
}
87+
});
9388
});
9489
let dataLocales = {};
9590
// explicitly list the source locale as having 100% coverage
@@ -112,11 +107,10 @@ function fetchTranslations(options) {
112107
}
113108

114109
function getResource(resourceId, callback) {
115-
let resourceURL = `${projectURL}/resource/${resourceId}`;
116-
getLanguages(resourceURL, (err, codes) => {
110+
getLanguages((err, codes) => {
117111
if (err) return callback(err);
118112

119-
asyncMap(codes, getLanguage(resourceURL), (err, results) => {
113+
asyncMap(codes, getLanguage(resourceId), (err, results) => {
120114
if (err) return callback(err);
121115

122116
let locale = {};
@@ -216,44 +210,48 @@ function fetchTranslations(options) {
216210
}
217211
}
218212

219-
220-
function getLanguage(resourceURL) {
221-
return (code, callback) => {
222-
code = code.replace(/-/g, '_');
223-
let url = `${resourceURL}/translation/${code}`;
224-
if (options.translReviewedOnly &&
225-
(!Array.isArray(options.translReviewedOnly) || options.translReviewedOnly.indexOf(code) !== -1)) {
226-
227-
url += '?mode=reviewed';
213+
function getLanguage(resourceId) {
214+
return async (code, callback) => {
215+
try {
216+
code = code.replace(/-/g, '_');
217+
let reviewedOnly = options.translReviewedOnly && (
218+
!Array.isArray(options.translReviewedOnly)
219+
|| options.translReviewedOnly.indexOf(code) !== -1);
220+
const url = await transifexApi.ResourceTranslationsAsyncDownload.download({
221+
resource: {data:{type:'resources', id:`o:${options.translOrgId}:p:${options.translProjectId}:r:${resourceId}`}},
222+
language: {data:{type:'languages', id:`l:${code}`}},
223+
// fetch only reviewed strings for some languages
224+
mode: reviewedOnly ? 'reviewed' : 'default'
225+
});
226+
const data = await fetch(url).then(d => d.text());
227+
process.stdout.write(`got translations for ${resourceId}, language ${code}\n`);
228+
callback(null, YAML.load(data)[code]);
229+
} catch (err) {
230+
process.stderr.write(`error while getting translations for ${resourceId}, language ${code}\n`);
231+
callback(err);
228232
}
229-
fetch(url, fetchOpts)
230-
.then(res => {
231-
process.stdout.write(`${res.status}: ${url}\n`);
232-
return res.json();
233-
})
234-
.then(json => {
235-
callback(null, YAML.load(json.content)[code]);
236-
})
237-
.catch(err => callback(err));
238233
};
239234
}
240235

241236

242-
function getLanguages(resourceURL, callback) {
243-
let url = `${resourceURL}?details`;
244-
fetch(url, fetchOpts)
245-
.then(res => {
246-
process.stdout.write(`${res.status}: ${url}\n`);
247-
return res.json();
248-
})
249-
.then(json => {
250-
callback(null, json.available_languages
251-
.map(d => d.code.replace(/_/g, '-'))
252-
// we already have the source locale so don't download it
253-
.filter(d => d !== options.sourceLocale)
254-
);
255-
})
256-
.catch(err => callback(err));
237+
async function getLanguages(callback) {
238+
try {
239+
const result = [];
240+
const project = await transifexApi.Project.get({
241+
organization: `o:${options.translOrgId}`,
242+
slug: options.translProjectId
243+
});
244+
const lngs = await project.fetch('languages');
245+
for await (const lng of lngs.all()) {
246+
if (lng.attributes.code === 'en') continue;
247+
result.push(lng.attributes.code.replace(/_/g, '-'));
248+
}
249+
process.stdout.write('got project languages\n');
250+
callback(null, result);
251+
} catch (err) {
252+
process.stderr.write('error while getting project languages\n');
253+
callback(err);
254+
}
257255
}
258256
}
259257

@@ -269,7 +267,7 @@ function asyncMap(inputs, func, callback) {
269267
function next() {
270268
callFunc(index++);
271269
if (index < inputs.length) {
272-
setTimeout(next, 200);
270+
setTimeout(next, 50);
273271
}
274272
}
275273

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"type": "module",
33
"name": "@ideditor/schema-builder",
4-
"version": "5.1.1",
4+
"version": "5.2.0-dev",
55
"description": "Framework for defining iD-compatible tagging models",
66
"homepage": "https://github.com/ideditor/schema-builder#readme",
77
"bugs": "https://github.com/ideditor/schema-builder/issues",
@@ -12,7 +12,7 @@
1212
"license": "ISC",
1313
"exports": "./lib/index.js",
1414
"dependencies": {
15-
"btoa": "^1.2.1",
15+
"@transifex/api": "^4.2.5",
1616
"chalk": "^5.0.1",
1717
"glob": "^8.0.3",
1818
"js-yaml": "^4.0.0",

0 commit comments

Comments
 (0)